<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
    <head>
        <!-- Book generated using mdBook -->
        <meta charset="UTF-8">
        <title>Rauthy Documentation</title>
        <meta name="robots" content="noindex">


        <!-- Custom HTML head -->

        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff">

        <link rel="icon" href="favicon.svg">
        <link rel="shortcut icon" href="favicon.png">
        <link rel="stylesheet" href="css/variables.css">
        <link rel="stylesheet" href="css/general.css">
        <link rel="stylesheet" href="css/chrome.css">
        <link rel="stylesheet" href="css/print.css" media="print">

        <!-- Fonts -->
        <link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="fonts/fonts.css">

        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" id="highlight-css" href="highlight.css">
        <link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
        <link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">

        <!-- Custom theme stylesheets -->
        <link rel="stylesheet" href="./mdbook-admonish.css">


        <!-- Provide site root and default themes to javascript -->
        <script>
            const path_to_root = "";
            const default_light_theme = "light";
            const default_dark_theme = "navy";
        </script>
        <!-- Start loading toc.js asap -->
        <script src="toc.js"></script>
    </head>
    <body>
    <div id="mdbook-help-container">
        <div id="mdbook-help-popup">
            <h2 class="mdbook-help-title">Keyboard shortcuts</h2>
            <div>
                <p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
                <p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
                <p>Press <kbd>?</kbd> to show this help</p>
                <p>Press <kbd>Esc</kbd> to hide this help</p>
            </div>
        </div>
    </div>
    <div id="body-container">
        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script>
            try {
                let theme = localStorage.getItem('mdbook-theme');
                let sidebar = localStorage.getItem('mdbook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script>
            const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
            let theme;
            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            const html = document.documentElement;
            html.classList.remove('light')
            html.classList.add(theme);
            html.classList.add("js");
        </script>

        <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">

        <!-- Hide / unhide sidebar before it is displayed -->
        <script>
            let sidebar = null;
            const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            } else {
                sidebar = 'hidden';
            }
            sidebar_toggle.checked = sidebar === 'visible';
            html.classList.remove('sidebar-visible');
            html.classList.add("sidebar-" + sidebar);
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <!-- populated by js -->
            <mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
            <noscript>
                <iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
            </noscript>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle">
                <div class="sidebar-resize-indicator"></div>
            </div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky">
                    <div class="left-buttons">
                        <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </label>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                        <button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
                            <i class="fa fa-search"></i>
                        </button>
                    </div>

                    <h1 class="menu-title">Rauthy Documentation</h1>

                    <div class="right-buttons">
                        <a href="print.html" title="Print this book" aria-label="Print this book">
                            <i id="print-button" class="fa fa-print"></i>
                        </a>

                    </div>
                </div>

                <div id="search-wrapper" class="hidden">
                    <form id="searchbar-outer" class="searchbar-outer">
                        <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
                    </form>
                    <div id="searchresults-outer" class="searchresults-outer hidden">
                        <div id="searchresults-header" class="searchresults-header"></div>
                        <ul id="searchresults">
                        </ul>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script>
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <main>
                        <p><img src="./rauthy_grey_small.png" alt="Rauthy Logo" /></p>
<h1 id="rauthy"><a class="header" href="#rauthy">Rauthy</a></h1>
<p>Rauthy - OpenID Connect Single Sign-On Identity &amp; Access Management</p>
<h2 id="what-it-is"><a class="header" href="#what-it-is">What it is</a></h2>
<p>Rauthy is a lightweight and easy to use OpenID Connect Identity Provider. It aims to be simple to both set up and
operate, with very secure defaults and lots of config options, if you need the flexibility. It puts heavy emphasis on
Passkeys and a very strong security in general. The project is written in Rust to be as memory efficient, secure and
fast as possible, and it can run on basically any hardware. If you need Single Sign-On support for IoT or headless
CLI tools, it's got you covered as well.<br />
You get High-Availability, client branding, UI translation, a nice Admin UI, Events and Auditing, and many more
features. By default, it runs on top of <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> and does not depend on an external
database (Postgres as an alternative) to make it even simpler to operate, while scaling up to millions of users easily.</p>
<h3 id="secure-by-default"><a class="header" href="#secure-by-default">Secure by default</a></h3>
<p>It tries to be as secure as possible by default while still providing all the options needed to be compatible with
older systems. For instance, if you create a new OIDC client, it activates <code>ed25519</code> as the default algorithm for
token signing and S256 PKCE flow. This will not work with clients, which do not support it, but you can of course
deactivate this to your liking.</p>
<h3 id="mfa-and-passwordless-login"><a class="header" href="#mfa-and-passwordless-login">MFA and Passwordless Login</a></h3>
<p><strong>Option 1:</strong><br />
Password + Security Key (without User Verification):<br />
Rauthy provides FIDO 2 / Webauthn login flows. If you once logged in on a new client with your username + password,
you will get an encrypted cookie which will allow you to log in without a password from that moment on. You only need to
have a FIDO compliant Passkey being registered for your account.</p>
<p><strong>Option 2:</strong><br />
Passkey-Only Accounts:<br />
Rauthy supports Passkey-Only-Accounts: you basically just provide your E-Mail address and log in with
your FIDO 2 Passkey. Your account will not even have / need a password. This login flow is restricted though to only
those passkeys, that can provide User Verification (UV) to always have at least 2FA security.</p>
<blockquote>
<p>Discoverable credentials are discouraged with Rauthy (for good reason). This means you will need to enter your E-Mail
for the login (which will be autofilled after the first one), but Rauthy passkeys do not use any storage on your
device. For instance when you have a Yubikey which can store 25 passkeys, it will not use a single slot there even
having full support.</p>
</blockquote>
<h3 id="fast-and-efficient"><a class="header" href="#fast-and-efficient">Fast and efficient</a></h3>
<p>The main goal was to provide an SSO solution like Keycloak and others while using a way lower footprint and being more
efficient with resources. For instance, Rauthy can easily run a fully blown SSO provider on just a Raspberry Pi. It
makes extensive use of caching for everything used in the authentication chain to be as fast as possible. Most things
are even cached for several hours and special care has been taken into account in case of cache eviction and
invalidation.</p>
<p>Rauthy comes with two database options:</p>
<ul>
<li>with embedded <a href="https://github.com/sebadob/hiqlite">Hiqlite</a>, which is the default setting</li>
<li>or you can optionally use a Postgres as your database, if you already have an instance running anyway.</li>
</ul>
<p>The resource usage depends a lot on your setup (Hiqlite, Postgres, HA deployment, amount of users, ...), but for a small
set of users, it is usually below 100mb of memory even with the very aggressive, in-memory caching Rauthy uses, and in
some cases even below 50mb.</p>
<h3 id="highly-available"><a class="header" href="#highly-available">Highly Available</a></h3>
<p>Even though it makes extensive use of caching, you can run it in HA mode. <a href="https://github.com/sebadob/hiqlite">Hiqlite</a>
creates its own embedded HA cache and persistence layer. Such a deployment is possible with
both <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> and Postgres.</p>
<h3 id="admin-ui--user-account-dashboard"><a class="header" href="#admin-ui--user-account-dashboard">Admin UI + User Account Dashboard</a></h3>
<p>Rauthy does have an Admin UI which can be used to basically do almost any operation you might need to administrate the
whole application and its users. There is also an account dashboard for each individual user, where users will get a
basic overview over their account and can self-manage som values, password, passkeys, and so on.</p>
<p><img src="https://github.com/sebadob/rauthy/blob/a89a8e9712c567551cb2d25b9da8823e35794f0a/frontend/screenshots/users.png" alt="Admin UI" /></p>
<p><img src="https://github.com/sebadob/rauthy/blob/a89a8e9712c567551cb2d25b9da8823e35794f0a/frontend/screenshots/account.png" alt="Account Dashboard" /></p>
<h3 id="client-branding"><a class="header" href="#client-branding">Client Branding</a></h3>
<p>You have a simple way to create a branding or stylized look for the Login page for each client. The whole color theme
can be changed and each client can have its own custom logo. Additionally, if you modify the branding for the default
<code>rauthy</code> client, it will not only change the look for the Login page, but also for the Account and Admin page.</p>
<p><img src="https://github.com/sebadob/rauthy/blob/c10e9421e65f386718528b15e3d0ace37aff1158/frontend/screenshots/branding.png" alt="Client Branding" /></p>
<h3 id="events-and-auditing"><a class="header" href="#events-and-auditing">Events and Auditing</a></h3>
<p>Rauthy comes with an Event- and Alerting-System. Events are generated in all kinds of scenarios. They can be sent via
E-Mail, Matrix or Slack, depending on the severity and the configured level. You will see them in the Admin UI in
real-time, or you can subscribe to the events stream and externally handle them depending on your own business logic.</p>
<h3 id="brute-force-and-basic-dos-protection"><a class="header" href="#brute-force-and-basic-dos-protection">Brute-Force and basic DoS protection</a></h3>
<p>Rauthy has brute-force and basic DoS protection for the login endpoint. The timeout will be artificially delayed after
enough invalid logins. It auto-blacklists IPs that exceeded too many invalid logins, with automatic expiry of the
blacklisting. You can, if you like, manually blacklist certain IPs as well via the Admin UI.</p>
<h3 id="iot-ready"><a class="header" href="#iot-ready">IoT Ready</a></h3>
<p>With the possibility to run on devices with very limited resources and having compatibility for the OAuth Device
Authorization Grant <code>device_code</code> flow, Rauthy would be a very good choice for IoT projects. The IdP itself can easily
run on a Raspberry Pi and all headless devices can be authenticated via the <code>device_code</code> flow. The <code>rauthy-client</code>
has everything built-in and ready, if you want to use Rust on the IoT devices as well. It has not been checked in a
<code>no_std</code> environment yet, but the client implementation is pretty simple.</p>
<h3 id="scales-to-millions-of-users"><a class="header" href="#scales-to-millions-of-users">Scales to millions of users</a></h3>
<p>Rauthy has no issue handling even millions of users. Everything keeps being fast and responsive, apart from the search
function for users in der Admin UI when you reach the 10+ million users, where searching usually takes ~3 seconds
(depending on your server of course).<br />
The only limiting factor at that point will be your configuration and needs for password hashing security. It really
depends on how many resources you want to use for hashing (more resources == more secure) and how many concurrent logins
at the exact same time you need to support.</p>
<h3 id="features-list"><a class="header" href="#features-list">Features List</a></h3>
<ul>
<li><input disabled="" type="checkbox" checked=""/>
Fully working OIDC provider</li>
<li><input disabled="" type="checkbox" checked=""/>
<a href="https://github.com/sebadob/hiqlite">Hiqlite</a> or Postgres as database</li>
<li><input disabled="" type="checkbox" checked=""/>
Fast and efficient with low footprint</li>
<li><input disabled="" type="checkbox" checked=""/>
Secure default values</li>
<li><input disabled="" type="checkbox" checked=""/>
Highly configurable</li>
<li><input disabled="" type="checkbox" checked=""/>
High-Availability</li>
<li><input disabled="" type="checkbox" checked=""/>
True passwordless accounts with E-Mail + Magic Link + Passkey</li>
<li><input disabled="" type="checkbox" checked=""/>
Dedicated Admin UI</li>
<li><input disabled="" type="checkbox" checked=""/>
Account dashboard UI for each user with self-service</li>
<li><input disabled="" type="checkbox" checked=""/>
OpenID Connect Dynamic Client Registration</li>
<li><input disabled="" type="checkbox" checked=""/>
OpenID Connect RP Initiated Logout</li>
<li><input disabled="" type="checkbox" checked=""/>
OpenID Connect Backchannel Logout</li>
<li><input disabled="" type="checkbox" checked=""/>
OAuth 2.0 Device Authorization Grant flow</li>
<li><input disabled="" type="checkbox" checked=""/>
Upstream Authentication Providers (Login with ...)</li>
<li><input disabled="" type="checkbox" checked=""/>
DPoP tokens for decentralized login flows</li>
<li><input disabled="" type="checkbox" checked=""/>
Ephemeral, dynamic clients for decentralized login flows</li>
<li><input disabled="" type="checkbox" checked=""/>
SCIM v2 for downstream clients</li>
<li><input disabled="" type="checkbox" checked=""/>
All End-User facing sites support i18n server-side translation
with the possibility to add more languages</li>
<li><input disabled="" type="checkbox" checked=""/>
Simple per client branding for the login page</li>
<li><input disabled="" type="checkbox" checked=""/>
Custom roles</li>
<li><input disabled="" type="checkbox" checked=""/>
Custom groups</li>
<li><input disabled="" type="checkbox" checked=""/>
Custom scopes</li>
<li><input disabled="" type="checkbox" checked=""/>
Custom user attributes</li>
<li><input disabled="" type="checkbox" checked=""/>
User attribute binding to custom scopes</li>
<li><input disabled="" type="checkbox" checked=""/>
Configurable password policy</li>
<li><input disabled="" type="checkbox" checked=""/>
Admin API Keys with fine-grained access rights</li>
<li><input disabled="" type="checkbox" checked=""/>
Events and alerting system</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional event persistence</li>
<li><input disabled="" type="checkbox" checked=""/>
Dedicated <code>forward_auth</code> endpoint, in addition to the existing userinfo,
with support for configurable trusted auth headers</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional event notifications via: E-Mail, Matrix, Slack</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional Force MFA for the Admin UI</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional Force MFA for each individual client</li>
<li><input disabled="" type="checkbox" checked=""/>
Additional encryption inside the database for the most critical entries</li>
<li><input disabled="" type="checkbox" checked=""/>
Automatic database backups with configurable retention and auto-cleanup (SQLite only)</li>
<li><input disabled="" type="checkbox" checked=""/>
auto-encrypted backups (<a href="https://github.com/sebadob/hiqlite">Hiqlite</a> only)</li>
<li><input disabled="" type="checkbox" checked=""/>
Ability to push <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> backups to S3 storage</li>
<li><input disabled="" type="checkbox" checked=""/>
auto-restore <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> backups from file or s3</li>
<li><input disabled="" type="checkbox" checked=""/>
Username enumeration prevention</li>
<li><input disabled="" type="checkbox" checked=""/>
Login / Password hashing rate limiting</li>
<li><input disabled="" type="checkbox" checked=""/>
Session client peer IP binding</li>
<li><input disabled="" type="checkbox" checked=""/>
IP blacklisting feature</li>
<li><input disabled="" type="checkbox" checked=""/>
Auto-IP blacklisting for login endpoints</li>
<li><input disabled="" type="checkbox" checked=""/>
Argon2ID with config helper UI utility</li>
<li><input disabled="" type="checkbox" checked=""/>
Housekeeping schedulers and cron jobs</li>
<li><input disabled="" type="checkbox" checked=""/>
JSON Web Key Set (JWKS) autorotation feature</li>
<li><input disabled="" type="checkbox" checked=""/>
Account conversions between traditional password and Passkey only</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional open user registration</li>
<li><input disabled="" type="checkbox" checked=""/>
Optional user registration domain restriction</li>
<li><input disabled="" type="checkbox" checked=""/>
App version update checker</li>
<li><input disabled="" type="checkbox" checked=""/>
SwaggerUI documentation</li>
<li><input disabled="" type="checkbox" checked=""/>
Configurable E-Mail templates for NewPassword + ResetPassword events</li>
<li><input disabled="" type="checkbox" checked=""/>
Prometheus <code>/metrics</code> endpoint on separate port</li>
<li><input disabled="" type="checkbox" checked=""/>
No-Setup migrations between different databases (Yes, even between <a href="https://github.com/sebadob/hiqlite">Hiqlite</a>
and Postgres)</li>
<li><input disabled="" type="checkbox" checked=""/>
Can serve a basic <code>webid</code> document</li>
<li><input disabled="" type="checkbox" checked=""/>
Experimental FedCM support</li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="getting-started"><a class="header" href="#getting-started">Getting Started</a></h1>
<h2 id="choose-a-database"><a class="header" href="#choose-a-database">Choose A Database</a></h2>
<p>Rauthy's default database is <a href="https://github.com/sebadob/hiqlite">Hiqlite</a>. Under the hood, it's using SQLite, but it
adds an additional layer on top making it highly-available using
the <a href="https://raft.github.io/">Raft Consensus Algorithm</a>. Don't let the SQLite engine under the hood fool you, it will
handle most probably anything you throw at it, as long as your disks are fast enough. Hiqlite can easily saturate a
1GBit/s network connection with just database (write) traffic. All reads are local, which means they are way faster than
with Postgres in any scenario.</p>
<p>If you already have a Postgres up an running with everything set up anyway, you might want to choose it as your main DB,
but I do not recommend deploying a Postgres instance just for Rauthy. This would be a waste of precious resources.</p>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="getting_started/main.html#admonition-hint"></a>
</div>
<div>
<p>If you want to migrate between databases at a later point, you can do this at any time very easily.
Just take a look at the <a href="getting_started/../config/config.html">Reference Config</a> and the variable <code>MIGRATE_DB_FROM</code>.</p>
</div>
</div>
<h2 id="container-images"><a class="header" href="#container-images">Container Images</a></h2>
<p>Rauthy versions before <code>0.27.0</code> had different container images depending on the database you choose. However, this is
not the case anymore. There is only a single image which works with any configuration. It works on <code>x86_64</code> and <code>arm64</code>
architectures.</p>
<p>At the time of writing, you can run Rauthy either with <a href="getting_started/./docker.html">Docker</a> or inside <a href="getting_started/./k8s.html">Kubernetes</a>.<br />
Both <em>Getting Started</em> guides do not cover all set up you might want to do for going into production. Especially the
<em>Docker</em> guide is more for testing.<br />
You should take a look at the <a href="getting_started/../config/config.html">Reference Config</a> to see, what you might want to configure
additionally. A dedicated <em>Going into production</em> guide will be written in the future.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="docker"><a class="header" href="#docker">Docker</a></h1>
<h2 id="testing--local-evaluation"><a class="header" href="#testing--local-evaluation">Testing / Local Evaluation</a></h2>
<p>For getting a first look at Rauthy, you can start it with docker (or any other container runtime) on your localhost.
The image contains a basic default config which is sufficient for local testing (don't use it in production, it
contains hardcoded secrets).</p>
<pre><code>docker run -it --rm -e LOCAL_TEST=true -p 8443:8443 ghcr.io/sebadob/rauthy:0.32.5
</code></pre>
<p>To proceed, go to <strong><a href="getting_started/first_start.html">First Start</a></strong>, or do the production setup below to have persistence.</p>
<h2 id="testing--evaluation-with-e-mail"><a class="header" href="#testing--evaluation-with-e-mail">Testing / Evaluation with E-Mail</a></h2>
<p>You can do anything you like with the default <code>admin@localhost</code> account. Rauthy does not have any special
accounts. It is an account like any other. The only reason it is a Rauthy admin, is because it is assigned to the
<code>rauthy_admin</code> role.</p>
<p>If you like to test creating new accounts or password reset flows though, you need to have at least a minimal setup that
is able to send E-Mails. The easiest way (works on localhost only) is the below <code>docker-compose.yaml</code>:</p>
<pre><code>networks:
  rauthy-test:
    driver: bridge
    
services:
  mailcrab:
    image: marlonb/mailcrab:latest
    ports:
      - "1080:1080"
    networks:
      - rauthy-test
      
  rauthy:
    container_name: rauthy-test
    image: ghcr.io/sebadob/rauthy:0.32.5
    environment:
      - LOCAL_TEST=true
      - SMTP_URL=mailcrab
      - SMTP_PORT=1025
      - SMTP_DANGER_INSECURE=true
    ports:
      - "8443:8443"
    depends_on:
      - mailcrab
    networks:
      - rauthy-test
</code></pre>
<p>Save this into <code>docker-compose.yaml</code> and start with:</p>
<pre><code>docker compose up -d
</code></pre>
<p>You then need the logs output from Rauthy to read the random password for the <code>admin@localhost</code> account:</p>
<pre><code>docker logs -f rauthy-test
</code></pre>
<p>Any outgoing E-Mail will be caught by <code>mailcrab</code>, which can be accessed
via <a href="http://localhost:1080">http://localhost:1080</a> .
When you are done testing, shut down with</p>
<pre><code>docker compose down
</code></pre>
<h2 id="production-setup"><a class="header" href="#production-setup">Production Setup</a></h2>
<p>For going to production or to test more in-depth, you need to create a config that matches your environment.</p>
<p>The first thing you want to do is to add a volume mount for the database. The second thing is to provide a more
appropriate config.</p>
<p>Rauthy expects at least the very basics in a config file. Most other values could be set via ENV vars. It parses both,
the config first and any env var that is set will overwrite a possibly existing one from the config. You can add
environment variables to the startup command with the <code>-e</code> option.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/docker.html#admonition-note"></a>
</div>
<div>
<p>The following commands will work on Linux and Mac OS (even though not tested on Mac OS). Since I am no Windows user
myself, I cannot provide tested commands in this case.</p>
</div>
</div>
<p><strong>1. We want to create a new directory for Rauthy's persistent data</strong></p>
<pre><code>mkdir -p rauthy/data
</code></pre>
<p><strong>2. Add the new config file.</strong></p>
<p>This documentation is in an early version and remote links are not available yet, they will be added at a later
point. For now, create a new file and paste the <a href="getting_started/../config/config.html">reference config</a></p>
<pre><code>vim rauthy/config.toml
</code></pre>
<p><strong>3. Access rights for the Database files</strong></p>
<p>The Rauthy container by default runs everything with user:group <code>10001:10001</code> for security reasons.<br />
To make this work with the default values, you have 2 options:</p>
<ul>
<li>Change the access rights:</li>
</ul>
<pre><code>sudo chmod 0600 rauthy/config.toml
sudo chmod 0700 rauthy/data
sudo chown -R 10001:10001 rauthy
</code></pre>
<ul>
<li>The other solution, if you do not have sudo rights, would be to change the owner of the whole directory.</li>
</ul>
<pre><code>chmod a+w rauthy/data
</code></pre>
<p>This will make the directory writeable for everyone, so Rauthy can create the database files inside the container
with <code>10001:10001</code> again.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="getting_started/docker.html#admonition-caution"></a>
</div>
<div>
<p>The safest approach would be to change the owner and group for these files on the host system. This needs <code>sudo</code>
to edit the config, which may be a bit annoying, but at the same time it makes sure, that you can only read
the secrets inside it with <code>sudo</code> too.</p>
<p>You should avoid making Rauthy's data world-accessible at all cost. <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> will
take care of this automatically during sub-directory creation, but the config includes sensitive information.</p>
</div>
</div>
<p><strong>4. Adopt the config to your liking.</strong></p>
<p>Take a look at the <a href="getting_started/../config/config.html">reference config</a> and adopt everything to your needs, but to not break this
example, be sure to not change <code>cluster.data_dir</code>.</p>
<p>For an in-depth guide on a production ready config, check the <a href="getting_started/../config/production_config.html">Production Config</a>
section.</p>
<p>A <strong>mandatory step</strong> will be to generate proper encryption keys. Take a look at <a href="getting_started/../config/encryption.html">Encryption</a>.</p>
<p><strong>5. Start the container with volume mounts</strong></p>
<pre><code>docker run -d \
    -v $(pwd)/rauthy/config.toml:/app/config.toml \
    -v $(pwd)/rauthy/data:/app/data \
    -p 8443:8443 \
    --name rauthy \
    ghcr.io/sebadob/rauthy:0.32.5
</code></pre>
<ul>
<li><code>-v $(pwd)/rauthy/config.toml:/app/config.toml</code> mounts the config in the correct place</li>
<li><code>-v $(pwd)/rauthy/data:/app/data</code> mounts the volume for Hiqlite</li>
<li><code>-p 8443:8443</code> needs to match your configured <code>server.port_http</code> or <code>server.port_https</code> of course. If you use a
reverse proxy inside a docker network, you don't need to expose any port, but you need to make sure to set
<code>server.scheme = "http"</code>, <code>server.proxy_mode = true</code> and the correct value for <code>server.trusted_proxies</code>.</li>
</ul>
<p><strong>6. You can now proceed with the <a href="getting_started/first_start.html">First Start</a> steps.</strong></p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="kubernetes"><a class="header" href="#kubernetes">Kubernetes</a></h1>
<p>At the time of writing, there is no Helm Chart or Kustomize files available yet. The whole setup is pretty simple
on purpose though, so it should not be a big deal to get it running inside Kubernetes.</p>
<h2 id="single-instance"><a class="header" href="#single-instance">Single Instance</a></h2>
<p>Since Rauthy uses pretty aggressive caching for different reasons, you cannot just have a single deployment and
scale up the replicas without a proper HA setup. How to deploy a HA version is described below.</p>
<p>The steps to deploy on Kubernetes are pretty simple.</p>
<ol>
<li>Create namespace</li>
<li>Create and apply the config</li>
<li>Create and apply the stateful set</li>
</ol>
<h3 id="create-namespace"><a class="header" href="#create-namespace">Create Namespace</a></h3>
<p>For the purpose of this documentation, we assume that Rauthy will be deployed in the <code>rauthy</code> namespace.<br />
If this is not the case for you, change the following commands accordingly.</p>
<pre><code>kubectl create ns rauthy
</code></pre>
<h3 id="create-and-apply-the-config"><a class="header" href="#create-and-apply-the-config">Create and apply the config</a></h3>
<p>This documentation will manage the Kubernetes files in a folder called <code>rauthy</code>.</p>
<pre><code>mkdir rauthy &amp;&amp; cd rauthy
</code></pre>
<p>Create the config file, paste the <a href="getting_started/../config/config.html">Reference Config</a> and adjust it to your needs. We are putting
the complete config in a K8s secret. Rauthy's config contains quite a few different secret values and it's just a lot
simpler to maintain everything in a single secret, than splitting it into a <code>ConfigMap</code> and overwrite each secret
manually.</p>
<pre><code>apiVersion: v1
kind: Secret
metadata:
  name: rauthy-config
  namespace: rauthy
type: Opaque
stringData:
  config.toml: |
    PASTE CONFIG HERE - WATCH THE INDENTATION'
</code></pre>
<p>Open the config with your favorite editor and paste the <a href="getting_started/../config/config.html">Reference Config</a> in place.<br />
Make sure to watch the indentation.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/k8s.html#admonition-note"></a>
</div>
<div>
<p>I recommend to just always set <code>cluster.node_id_from = "k8s"</code> when deploying a StatefulSet. This will parse the Raft
NodeID automatically from the K8s Pod / Hostname and you don't have to worry about the <code>node_id</code>. For instance, a Pod
named <code>rauthy-0</code> will be translated to <code>node_id = 1</code> automatically.</p>
</div>
</div>
<p>There are some values that you need to generate on your own. These are:</p>
<ul>
<li><code>cluster.secret_raft</code> + <code>cluster.secret_api</code></li>
<li><code>encryption.keys</code> + <code>encryption.key_active</code></li>
</ul>
<p>The secrets for the <code>cluster</code> can be just some long random alphanumeric values. They are used for authentication for the
Hiqlite Raft + API layer. The encryption keys must be generated. More detailed explanation is in
the <a href="getting_started/../config/encryption.html">Encryption</a> section. The tl;dr is:</p>
<pre><code>echo "$(openssl rand -hex 4)/$(openssl rand -base64 32)"
</code></pre>
<p>Copy the output and add it to keys. The <code>key_active</code> will be the first part of the output until the first <code>/</code>. For
instance:</p>
<pre><code class="language-toml">[encryption]
keys = ["XLCcaQ/f2xmq/nxVFgJN0CN311miyvVlBxXOQISyw1nPEPOqiI="]
key_active = "XLCcaQ"
</code></pre>
<p>You can generate safe values for both <code>secret_raft</code> and <code>secret_api</code> in many ways. You can just provide a random
alphanumeric value, which for instance:</p>
<pre><code>cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c48
</code></pre>
<p>or you can use the above <code>openssl</code> command again, even though Hiqlite does not need or utilize base64:</p>
<pre><code>openssl rand -base64 48
</code></pre>
<p>If you plan on using S3 for backups, paste the proper values into <code>cluster.s3_*</code> values.</p>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/k8s.html#admonition-note-1"></a>
</div>
<div>
<p>It seems that in some environments, the above <code>openssl</code> command does not output proper values, which will make Rauthy
panic on startup, when it checks the given values. If you run into that situation, you can generate them without
<code>openssl</code> as well, with e.g:</p>
<pre><code>echo "$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 6)/$(cat /dev/urandom | head -c 32 | base64)"</code></pre>
</div>
</div>
<h3 id="create-and-apply-the-stateful-set"><a class="header" href="#create-and-apply-the-stateful-set">Create and apply the stateful set</a></h3>
<pre><code>touch sts.yaml
</code></pre>
<p>Paste the following content into the <code>sts.yaml</code> file:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: rauthy
  namespace: rauthy
spec:
  selector:
    app: rauthy
  ports:
    # chose whatever fits your needs here, you usually only need either http or https
    - name: http
      port: 8080
      targetPort: 8080
    - name: https
      port: 8443
      targetPort: 8443
---
# The headless service is used for the Raft Cluster setup, so Nodes 
# can connect to each other without any load balancer in between.
apiVersion: v1
kind: Service
metadata:
  name: rauthy-headless
  namespace: rauthy
spec:
  type: ClusterIP
  clusterIP: None
  sessionAffinity: None
  selector:
    app: rauthy
  ports:
    - name: hiqlite-raft
      protocol: TCP
      port: 8100
      targetPort: 8100
    - name: hiqlite-api
      protocol: TCP
      port: 8200
      targetPort: 8200
---
# The PDB is only necessary for a HA deployment. You can take it out for a single instance.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: rauthy
  namespace: rauthy
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: rauthy
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rauthy
  namespace: rauthy
  labels:
    app: rauthy
spec:
  serviceName: rauthy-headless
  # If you start a fresh cluster without a bootstrapped Admin password, it is
  # highly suggested to start a single replica for the first setup + login.
  # It will work with 3 replicas directly, but if you are not quick enough and
  # your logs buffer size is small, you might miss the auto-generated password
  # in Pod `rauthy-0` because of many logs.
  replicas: 1
  selector:
    matchLabels:
      app: rauthy
  template:
    metadata:
      labels:
        app: rauthy
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - rauthy
              topologyKey: "kubernetes.io/hostname"
      securityContext:
        fsGroup: 10001
      containers:
        - name: rauthy
          image: ghcr.io/sebadob/rauthy:0.32.5
          securityContext:
            # User ID 10001 is actually built into the container 
            # at the creation for better security
            runAsUser: 10001
            runAsGroup: 10001
            allowPrivilegeEscalation: false
          ports:
            # Hiqlite internal ports
            - containerPort: 8100
            - containerPort: 8200
            # You may need to adjust this, if you decide to start 
            # in https only mode or use another port
            - containerPort: 8080
            - containerPort: 8443
          volumeMounts:
            - name: rauthy-data
              mountPath: /app/data
              readOnly: false
            - name: rauthy-config
              mountPath: /app/config.toml
              readOnly: true
              subPath: config.toml
          readinessProbe:
            httpGet:
              scheme: HTTP
              # adjust if you change the Raft API port
              port: 8200
              path: /ping
            initialDelaySeconds: 5
            periodSeconds: 1
          livenessProbe:
            httpGet:
              # You may need to adjust this, if you decide to start in https only
              # mode or use another port
              scheme: HTTP
              port: 8080
              #scheme: HTTPS
              #port: 8443
              path: /auth/v1/health
            initialDelaySeconds: 1
            periodSeconds: 30
          resources:
            requests:
              # Tune the memory requests value carefully. Make sure, that the
              # pods request at least:
              # `ARGON2_M_COST` / 1024 * `MAX_HASH_THREADS` Mi + idle memory
              # The actual usage also heavily depends on the Memory Allocator
              # tuning. You can find more information in the Tuning section
              # in this book.
              memory: 64Mi
              # The CPU needs to be adjusted during runtime. This heavily
              # depends on your use case.
              cpu: 100m
            limits:
            # Be careful with the memory limit. You must make sure, that the
            # (very costly) password hashing has enough memory available. If not,
            # the application will crash. You do not really need a memory limit,
            # since Rust is not a garbage collected language. Better take a close
            # look at what the container actually needs during
            # prime time and set the requested resources above properly.
            #memory:
            # A CPU limit may make sense in case of DDoS attacks or something
            # like this, if you do not have external rate limiting or other
            # mechanisms. Otherwise, `MAX_HASH_THREADS` is the main mechanism 
            # to limit resources.
            #cpu: 1000m
      volumes:
        - name: rauthy-config
          secret:
            secretName: rauthy-config
  volumeClaimTemplates:
    - metadata:
        name: rauthy-data
      spec:
        accessModes:
          - "ReadWriteOnce"
        resources:
          requests:
            storage: 128Mi
        #storageClassName: provideIfNeeded
</code></pre>
<h3 id="ingress"><a class="header" href="#ingress">Ingress</a></h3>
<p>This example assumes, that the deployment will run behind a Kubernetes ingress resource of your choice.</p>
<p>It uses <a href="https://doc.traefik.io/traefik/">Traefik</a> with the <code>IngressRoute</code> CRD.<br />
Nevertheless, the ingress is really simple, and it should be very easy to adopt anything else.</p>
<p>Create the <code>ingress.yaml</code></p>
<pre><code>touch ingress.yaml
</code></pre>
<p>Paste the following content into the <code>ingress.yaml</code> file and adjust to your needs</p>
<pre><code class="language-yaml">apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: rauthy-https
  namespace: rauthy
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`auth.example.com`)
      kind: Rule
      services:
        - name: rauthy
          port: 8080
</code></pre>
<h3 id="deploy"><a class="header" href="#deploy">Deploy</a></h3>
<p>We are now ready to deploy:</p>
<pre><code>kubectl apply -f .
</code></pre>
<p>And then to observe the deployment:</p>
<pre><code>kubectl -n rauthy get pod -w
</code></pre>
<p>You can now proceed with the <a href="getting_started/first_start.html">First Start</a> steps.</p>
<h2 id="production-setup-1"><a class="header" href="#production-setup-1">Production Setup</a></h2>
<h3 id="config"><a class="header" href="#config">Config</a></h3>
<p>Going to production does not need too many additional steps.</p>
<h4 id="tls-certificates"><a class="header" href="#tls-certificates">TLS Certificates</a></h4>
<p>The thing you need will be valid TLS certificates, of course. To get these, there are a lot of existing mechanisms. If
you use an internal Certificate Authority (CA), you do have you own tools to work with this anyway. If, however, you
want to use something like <a href="https://letsencrypt.org/de/">Let's Encrypt</a>, I suggest to use the
<a href="https://cert-manager.io/">cert-manager</a>, which is easy and straight forward to use.</p>
<p>An example, how to add a certificate for the Traefik IngressRoute from above:</p>
<pre><code class="language-yaml">apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: rauthy-https
  namespace: rauthy
spec:
  entryPoints:
    - websecure
  tls:
    # Paste the name of the TLS secret here
    secretName: secret-name-of-your-tls-certificate
  routes:
    - match: Host(`auth.example.com`)
      kind: Rule
      services:
        - name: rauthy
          port: 8080
</code></pre>
<p>You may want to add an HTTPS redirect as well:</p>
<pre><code class="language-yaml">apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: https-only
  namespace: rauthy
spec:
  redirectScheme:
    scheme: https
    permanent: true
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: rauthy-http
  namespace: rauthy
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`auth.example.com`)
      kind: Rule
      middlewares:
        - name: https-only
      services:
        - name: rauthy
          port: 8080
</code></pre>
<h4 id="hiqlite-internal-tls"><a class="header" href="#hiqlite-internal-tls">Hiqlite Internal TLS</a></h4>
<p>You can of course also provide TLS certificates for the Hiqlite internal communication. Two independent networks are
created: one for the Raft-Internal network traffic like heartbeats and data replication, and a second one for the
"external" Hiqlite API. This is used by other Hiqlite cluster members for management purposes and to execute things
like consistent queries on the leader node.</p>
<p>You can provide TLS certificates for both of them independently via the following config variables:</p>
<pre><code class="language-toml">[cluster]
# If given, these keys / certificates will be used to establish
# TLS connections between nodes.
#
# values are optional, overwritten by: HQL_TLS_{RAFT|API}_{KEY|CERT}
# overwritten by: HQL_TLS_RAFT_KEY
tls_raft_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_CERT
tls_raft_cert = "tls/tls.crt"
tls_raft_danger_tls_no_verify = true

# overwritten by: HQL_TLS_API_KEY
tls_api_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_KEY
tls_api_cert = "tls/tls.crt"
tls_api_danger_tls_no_verify = true
</code></pre>
<h4 id="additional-steps"><a class="header" href="#additional-steps">Additional steps</a></h4>
<p>There are a few more things to do when going into production, but these are the same for Kubernetes and Docker and will
be explained in later chapters.</p>
<p>You can now proceed with the <a href="getting_started/first_start.html">First Start</a> steps.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="first-start"><a class="header" href="#first-start">First Start</a></h1>
<h2 id="initial-admin-password-and-login"><a class="header" href="#initial-admin-password-and-login">Initial admin password and login</a></h2>
<p>With the very first start of rauthy, or better with an empty database, when rauthy is starting, it does not only
create all the necessary schemas and initial data, but also some sensitive information will be generated safely.
This includes a set of Json Web Keys (JWKS) for the token signing and some secrets.</p>
<p>The most important of these newly generated secrets is the default admin user's password.<br />
When this is securely generated with the very first start, it will be logged into the console. This will only
happen once and never again.</p>
<h3 id="logs-with-docker"><a class="header" href="#logs-with-docker">Logs with docker</a></h3>
<pre><code>docker logs -f rauthy
</code></pre>
<h3 id="logs-with-kubernetes"><a class="header" href="#logs-with-kubernetes">Logs with Kubernetes</a></h3>
<pre><code>kubectl -n rauthy logs -f rauthy-0
</code></pre>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/first_start.html#admonition-note"></a>
</div>
<div>
<p>If you do a Kubernets HA deployment directly, only the Pod <code>rauthy-0</code> will log the initial password.</p>
</div>
</div>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/first_start.html#admonition-note-1"></a>
</div>
<div>
<p>If you missed this log entry, you will not be able to log in.<br />
If this is the case, you can delete the database / volume and just restart rauthy.</p>
</div>
</div>
<p>The log message contains a link to the accounts page, where you then should log in to immediately set a new password.
Follow the link, use as the default admin <code>admin@localhost</code> and as password the copied value from the log.</p>
<ul>
<li>When logged into the account, click <code>EDIT</code> and <code>CHANGE PASSWORD</code> to set a new password</li>
<li>Log out of the account and try to log in to the admin ui with the new password</li>
</ul>
<h2 id="custom-rauthy-admin-user"><a class="header" href="#custom-rauthy-admin-user">Custom rauthy admin user</a></h2>
<p>It is a good idea, to either keep the <code>admin@localhost</code> and rename it (to never have a default admin, which would be an
attack vector) as a fallback user with just a very long password, or disable it, after a custom admin has been added.</p>
<p>When logged in to the admin UI, you can add a new user. When the <code>SMTP</code> settings are correctly configured in the config,
which we can test right now, you will receive an E-Mail with the very first password reset.</p>
<div id="admonition-note-2" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-2-title">
<div class="admonition-title">
<div id="admonition-note-2-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="getting_started/first_start.html#admonition-note-2"></a>
</div>
<div>
<p>If you do not receive an E-Mail after the first user registration, chances are you may have a problem with the SMTP
setup.<br />
To debug this, you can set <code>LOG_LEVEL=debug</code> in the config and then watch the logs after a restart.</p>
</div>
</div>
<h3 id="rauthy_admin-user-role"><a class="header" href="#rauthy_admin-user-role"><code>rauthy_admin</code> user role</a></h3>
<p>The role, which allows a user to access the admin UI, is the <code>rauthy_admin</code>.<br />
If the user has this role assigned, he will be seen as an admin.</p>
<p>Under the hood, rauthy itself uses the OIDC roles and groups in the same way, as all clients would do. This means you
should not neither delete the <code>rauthy</code> default client, nor the <code>rauthy_admin</code> role. There are mechanisms to prevents
this happening by accident via UI, but you could possibly do this via a direct API call.<br />
There are some anti-lockout mechanisms in place in the backend, which will be executed with every start, but being
careful at this point is a good idea anyway.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="production-config"><a class="header" href="#production-config">Production Config</a></h1>
<p>If you want to go into production, the configuration from the Getting Started section is most probably not enough.</p>
<p>The best thing you could do is just taking a look at the <a href="config/../config/config.html">Reference Config</a> and reading through
all the possible options.</p>
<p>However, this section will give you a head start with the minimum you should set up. Depending on if you started
with Docker or Kubernetes so far, some values might have been already set.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="minimal-production-config"><a class="header" href="#minimal-production-config">Minimal Production Config</a></h1>
<p>This section gives you a template for probably the most minimal config you could do when going into production.</p>
<p>You can either copy &amp; paste this (with adjustments of course) and expand it when necessary, or take a look at the
complete <a href="config/config.html">Reference Config</a> and check each existing value.</p>
<pre><code class="language-toml">[bootstrap]
# If set, the email of the default admin will be changed during
# the initialization of an empty production database.
#
# default: 'admin@localhost'
# overwritten by: BOOTSTRAP_ADMIN_EMAIL
admin_email = "admin@localhost"

[cluster]
# Can be set to 'k8s' to try to split off the node id from the hostname
# when Hiqlite is running as a StatefulSet inside Kubernetes.
#
# default: unset
# overwritten by: HQL_NODE_ID_FROM
#node_id_from = "k8s"

# The node id must exist in the nodes and there must always be
# at least a node with ID 1
# Will be ignored if `node_id_from = k8s`
#
# At least `node_id_from` or `node_id` are required.
#
# default: 0 (invalid)
# overwritten by: HQL_NODE_ID
node_id = 1

# All cluster member nodes. For a single instance deployment, 
# `"1 localhost:8100 localhost:8200"` will work just fine.
# Each array value must have the following format:
# `id addr_raft addr_api`
#
# default: ["1 localhost:8100 localhost:8200"]
# overwritten by: HQL_NODES
nodes = [
    "1 localhost:8100 localhost:8200",
    #    "2 localhost:8101 localhost:8201",
    #    "3 localhost:8102 localhost:8202",
]

# Secrets for Raft internal authentication as well as for the API.
# These must be at least 16 characters long and you should provide
# different ones for both variables.
#
# default: not set - required
# overwritten by: HQL_SECRET_RAFT
secret_raft = "SuperSecureSecret1337"
# default: not set - required
# overwritten by: HQL_SECRET_API
secret_api = "SuperSecureSecret1337"

[email]
# Rauthy will force TLS and try a downgrade to STARTTLS, if
# TLS fails. It will never allow an unencrypted connection.
# You might want to set `SMTP_DANGER_INSECURE=true` if you
# need this for local dev.
#
# overwritten by: SMTP_URL
smtp_url = "localhost"
# optional, default will be used depending on TLS / STARTTLS
# overwritten by: SMTP_PORT
#smtp_port = 
# overwritten by: SMTP_USERNAME
smtp_username = "username"
# overwritten by: SMTP_PASSWORD
smtp_password = "password"
# Format: "Rauthy &lt;rauthy@localhost&gt;"
# default: "Rauthy &lt;rauthy@localhost&gt;"
# overwritten by: SMTP_FROM
smtp_from = "Rauthy &lt;rauthy@localhost&gt;"

[encryption]
# You need to define at least one valid encryption key.
# These keys are used in various places, like for instance
# encrypting confidential client secrets in the database, or
# encryption cookies, and so on.
#
# The first part until the first `/` is the key ID.
# The ID must match '^[a-zA-Z0-9:_-]{2,20}$'
#
# The key itself begins after the first `/` has been found.
# The key must be exactly 32 bytes long, encoded as base64.
#
# You can find a more detailed explanation on how to generate
# keys in the documentation:
# 1. https://sebadob.github.io/rauthy/getting_started/k8s.html#create-and-apply-secrets
# 2. https://sebadob.github.io/rauthy/config/encryption.html
#
# You can provide multiple keys to make things like key
# rotation work. Be careful with removing old keys. Make sure
# that all secrets have been migrated beforehand.
# You can find a utility in the Admin UI to do this for you.
#
# overwritten by: ENC_KEYS - single String, \n separated values
keys = ["q6u26/M0NFQzhSSldCY01rckJNa1JYZ3g2NUFtSnNOVGdoU0E="]

# This identifies the key ID from the `ENC_KEYS` list, that
# should actively be used for new encryptions.
#
# overwritten by: ENC_KEY_ACTIVE
key_active = "q6u26"

[events]
# The E-Mail address event notifications should be sent to.
#
# overwritten by: EVENT_EMAIL
email = "admin@localhost"

[server]
# The scheme to use locally, valid values:
# http | https | http_https | unix_http | unix_https
# For more details about the UNIX domain socket, check out its
# documentation page.
#
# default: http_https
# overwritten by: LISTEN_SCHEME
scheme = "http"

# The Public URL of the whole deployment
# The LISTEN_SCHEME + PUB_URL must match the HTTP ORIGIN HEADER
# later on, which is especially important when running Rauthy
# behind a reverse proxy. In case of a non-standard port (80/443),
# you need to add the port to the PUB_URL
#
# default: not set - mandatory
# overwritten by: PUB_URL
pub_url = "localhost:8080"

# When rauthy is running behind a reverse proxy, set to true
#
# default: false
# overwritten by: PROXY_MODE
proxy_mode = false

# A list of trusted proxy CIDRs. When `proxy_mode = true`
# or `peer_ip_header_name` is set, these are mandatory to
# be able to extract the real client IP properly and safely
# to prevent IP header spoofing. All requests witha
# different source will be blocked.
#
# default: []
# overwritten by: TRUSTED_PROXIES - single String, \n separated values
#trusted_proxies = ['192.168.14.0/24']

[webauthn]
# The 'Relaying Party (RP) ID' - effective domain name.
#
# CAUTION: When this changes, already registered devices will
# stop working and users cannot log in anymore!
#
# default: 'localhost'
# overwritten by: RP_ID
rp_id = "localhost"

# Url containing the effective domain name.
#
# DEV: If you want to test Webauthn via the Svelte DEV UI,
# change the port number to :5173.
#
# !!! CAUTION: Must ALWAYS include the port number !!!
#
# default: 'http://localhost:8080'
# overwritten by: RP_ORIGIN
rp_origin = "http://localhost:5173"
</code></pre>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="config/config_minimal.html#admonition-hint"></a>
</div>
<div>
<p>TL;DR for generating a <code>encryption.keys</code>:</p>
<pre><code>echo "$(openssl rand -hex 4)/$(openssl rand -base64 32)"</code></pre>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="password-hashing-setup"><a class="header" href="#password-hashing-setup">Password Hashing Setup</a></h1>
<p>Even though the options and tools in the Admin UI should be fully documented, I wanted to mention argon2id tuning here.</p>
<p>Rauthy uses the argon2id hashing algorithm for passwords. This is the most expensive and compute-heavy operation
done by the application and the variables need to be tuned for every deployment to provide the best compromise
of security, resource usage and user experience.<br />
The default values are a good start at the lower end of the "being safe scale" for a production deployment.
However, you should at least take a look at them and verify, that they work for you.</p>
<p>The Admin UI provides a utility which helps you find the best values for your deployment quickly. What and how to do is
described in the Admin UI itself, I just want to guide you to this utility especially, since it is an important step
security wise.</p>
<p>When you are logged in to the Admin UI, please navigate to <code>Config</code> -&gt; <code>Argon2 Parameters</code> to find your values.
After they have been found, apply them to the Rauthy config and restart the deployment.<br />
Keep in mind, that if you run the application in a way, where memory is limited, for instance inside Kubernetes with
resource limits set too low, that it will crash, if either <code>hashing.argon2_m_cost</code> is set too high or the memory limit
too low.</p>
<p>There is one additional, really important config variable need to be taken into account for the tuning.<br />
Since this operation is really resource intense, you can limit the amount of threads, which can run in parallel doing
hashing operations. This is really important when we think about constrained memory again.</p>
<p><code>hashing.max_hash_threads</code> limits the maximum amount of parallel password hashes at the exact same time to never exceed
system memory while still allowing a good amount of memory. The <strong>default value is 2</strong>.</p>
<p>The rule is simple: Allow as many resources as possible for hashing to have the maximum amount of security, while
restricting it as much as necessary.</p>
<p>For smaller deployments, set <code>hashing.max_hash_threads</code>, which will technically allows only one user login at the exact
same time. This value makes an external rate limiting for the login obsolete (while you may add some for the others).</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="passkeys--webauthn"><a class="header" href="#passkeys--webauthn">Passkeys / WebAuthn</a></h1>
<h2 id="passkey-only-accounts"><a class="header" href="#passkey-only-accounts">Passkey Only Accounts</a></h2>
<p>Rauthy provides the ability to create Passkey only accounts.<br />
These accounts do not have any password at all. The user can login via E-Mail + providing the Passkey. Only keys
with additional user verification (UV) will be accepted for these accounts. This makes sure, that they are 2FA / MFA
secured (depending on the device) all the time.</p>
<p>You can choose the account type either during the initial password reset link you get via E-Mail, or you can
convert a traditional password account to a passkey only account in your account view, if you have at least
one Passkey with additional UV registered.</p>
<p>Passkey only accounts provide a few benefits:</p>
<ul>
<li>no need to remember or store any passwords</li>
<li>way easier and faster logging in</li>
<li>always 2FA / MFA</li>
<li>strongest type of authentication</li>
<li>no need to satisfy password policies</li>
<li>no need to reset your password after it has been expired</li>
</ul>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-caution"></a>
</div>
<div>
<p>Passkey only accounts cannot use the traditional password reset E-Mails.</p>
<p>This is a drawback and a benefit at the same time:<br />
No way to take over an account if the E-Mail account has been compromised, but at the same time the user
relies on an Admin to reset the MFA devices, if no backup exists or all are lost.</p>
</div>
</div>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-note"></a>
</div>
<div>
<p>Unfortunately, passkeys have one big drawback when you use the discoverable credentials / resident keys feature.
This is really cool and the best possible UX at first glance, because the user does not need to provide even a
username / email manually, but has one very big drawback.</p>
<p><strong>The amount of resident keys a passkey can store is always limited.</strong></p>
<p>There are devices, that can get locked up completely if you try to register resident keys beyond the capacity of
the device itself. For this reason, Rauthy strictly discourages the use of resident keys and will always request the
user to provide the email, so you can never end up in such a situation with Rauthy. Non-discoverable credentials are
not limited per device.</p>
<p>Rauthy remembers the last email that has been used for a login on each machine. This provides the same best possible
UX in the end that you would get with discoverable credentials but without the drawbacks of the limited amount.</p>
</div>
</div>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-info"></a>
</div>
<div>
<p>Android has finally added support for biometric UV in September 2023.<br />
This has made is possible to implement this feature into Rauthy without sacrificing security.</p>
<p>However, at the time of writing (2024/06/05), PIN / biometric UV via NFC keys like Yubikeys does still not work.</p>
<p>Sadly, Google just decided to not implement this feature for other keys than their own implementation of it.</p>
</div>
</div>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-note-1"></a>
</div>
<div>
<p>On macOS, when your browser tries to create a passkey, it will default to using the built-in Passwords app. If you wish
to use a different password manager such as 1Password, Vaultwarden, etc., you'll need to disable the setting "Autofill
Passwords and Passkeys".</p>
<p><img src="config/img/macos_autofill.png" alt="the setting Autofill Passwords and Passkeys is under a section called AutoFill and Passwords" /></p>
</div>
</div>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-hint"></a>
</div>
<div>
<p>If you want to register an Android device for a Passkey only account, but you are using for instance Yubikeys
with PIN UV, you can do the following trick to get it done (works only with the latest Play store version):</p>
<ul>
<li>Create a password for your account, if it does not exist yet</li>
<li>Remove all registered passkeys</li>
<li>Log in to your account view on your Android device and another device that works with your Yubikey</li>
<li>With both devices logged in at the same time:
<ul>
<li>Register a new passkey with Android and choose "this device", which will create a Passkey flow with your fingerprint</li>
<li>Register the Yubikey on the other device</li>
</ul>
</li>
<li>You should now have 2 Passkeys: Android + Yubikey</li>
<li>Navigate to the Password page inside your account on any device and convert it to Passkey only</li>
<li>You should now be able to log in on your Android device with Passkey only and with your Yubikey</li>
</ul>
</div>
</div>
<h2 id="config-1"><a class="header" href="#config-1">Config</a></h2>
<p>You should use Passkeys / Webauthn in production for 2FA / MFA.
To make sure it works, you need to check your the config.</p>
<p>Adjust the following variables in your config, to make sure it works correctly.</p>
<h3 id="rp_id"><a class="header" href="#rp_id"><code>RP_ID</code></a></h3>
<p>This is the <em>Relaying Party (RP) ID</em>, which should be your effective domain name.<br />
Let's say our application is available at <code>auth.example.com</code>, then this should be:</p>
<pre><code class="language-toml">[webauthn]
rp_id = "auth.example.com"
</code></pre>
<div id="admonition-caution-1" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-1-title">
<div class="admonition-title">
<div id="admonition-caution-1-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-caution-1"></a>
</div>
<div>
<p>When the <code>RP_ID</code> changes, already registered devices will stop working and users cannot log in anymore!
Be very careful, if you want / need to do this in production.</p>
</div>
</div>
<h3 id="rp_origin"><a class="header" href="#rp_origin"><code>RP_ORIGIN</code></a></h3>
<p>The second important variable is the <code>webauthn.rp_origin</code>. This needs to be set to the scheme + URL containing the
effective domain name + port.</p>
<div id="admonition-caution-2" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-2-title">
<div class="admonition-title">
<div id="admonition-caution-2-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/passkeys.html#admonition-caution-2"></a>
</div>
<div>
<p>The <code>rp_origin</code> must always include the port number, even if it is just the default 443 for HTTPS.</p>
</div>
</div>
<p>In this example, assuming Rauthy will be available at port 443, correct would be:</p>
<pre><code class="language-toml">[webauthn]
rp_origin = https://auth.example.com:443
</code></pre>
<h3 id="rp_name"><a class="header" href="#rp_name"><code>RP_NAME</code></a></h3>
<p>This variable can be set to anything "pretty".<br />
This may be shown to the user in a way like "<code>rp_name</code> requests your security key ...". If this is shown depends on the
OS and the browser the client uses. Firefox, for instance, does not show this at the time of writing.</p>
<p>You can change the <code>rp_name</code> later on without affecting the validation of already registered keys.</p>
<h3 id="webauthn_renew_exp"><a class="header" href="#webauthn_renew_exp"><code>WEBAUTHN_RENEW_EXP</code></a></h3>
<p>For all non Passkey only accounts, Rauthy will always prompt a user at least once for the password on a new machine,
even with active
passkeys. The keys are used either as a strong second factor, when they do not work with a PIN, or bump up the whole
login to real MFA, if the OS / Browser / Key does support this.</p>
<p>When a user has logged in successfully on a new device and active 2FA / MFA, Rauthy will set an encrypted cookie as a
"remember me". The lifetime of this cookie can be configured with <code>renew_exp</code>. The <strong>default</strong> of this value is <strong>2160
hours</strong>.</p>
<p>As long as this cookie is present and can be decrypted by the backend, the user can log in from this very
device with the registered Passkey key only, which makes a very good user experience for the whole login flow.
The E-Mail will already be filled automatically and only a single click on the login button is necessary.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="encryption"><a class="header" href="#encryption">Encryption</a></h1>
<p>Rauthy does additional encryption for values in lots of places, like for instance <code>client_secret</code>s in the database
or session cookies. In the Getting Started for Kubernetes, we have set up the <code>encryption.keys</code> and
<code>encryption.key_active</code>.</p>
<p>The <code>encryption.keys</code> defines the static keys used for additional data encryption in a few places. This values may
contain multiple keys, if you want to rotate them at some point without breaking the decryption of all already existing
secrets.</p>
<p><code>encryption.key_active</code> defines the key inside <code>encryption.keys</code> which will be used as the default. This means that all
new / current encryption's performed by the backend will use the key with the given ID.</p>
<h2 id="setup"><a class="header" href="#setup">Setup</a></h2>
<p>If you followed the Getting Started for Kubernetes, you already completed this step. If not, this is how you can
generate an encryption key.</p>
<pre><code>echo "$(openssl rand -hex 4)/$(openssl rand -base64 32)"
</code></pre>
<p>The first part until the first <code>/</code> is the key ID. This has to be between 2 and 20 characters and should not contain any
special ones. The second part after the first <code>/</code> is the key itself. This value <strong>must</strong> be exactly 32 bytes long and
then be base64 encoded. If it is not, Rauthy will yell at startup and panic early.</p>
<p>If you have generated a key, lets say the output was</p>
<pre><code>❯ echo "$(openssl rand -hex 4)/$(openssl rand -base64 32)"
90eb6d69/U9wZG4GS/94pVh6iTH1ijf+kj+tXJHKkQNsp5eImMQI=
</code></pre>
<p>Your config value should look like this:</p>
<pre><code class="language-toml">[encryption]
keys = ["90eb6d69/U9wZG4GS/94pVh6iTH1ijf+kj+tXJHKkQNsp5eImMQI="]
key_active = "90eb6d69"
</code></pre>
<p>You can add more keys if you like, <strong>separated by new lines</strong>, which is needed for the key rotation described below.
The <code>encryption.key_active</code> will be the key being used for all new encryption's.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/encryption.html#admonition-note"></a>
</div>
<div>
<p>It seems that in some environments, the above <code>openssl</code> command does not output proper values, which will make Rauthy
panic on startup, when it checks the given values. If you run into that situation, you can generate them without
<code>openssl</code> as well, with e.g:</p>
<pre><code>echo"$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 6)/$(cat /dev/urandom | head -c 32 | base64)"</code></pre>
</div>
</div>
<h2 id="key-rotation"><a class="header" href="#key-rotation">Key Rotation</a></h2>
<p>A good practice is to rotate encryption keys from time to time. Rauthy does auto-rotate the keys for signing tokens,
but the admin is responsible for the encryption keys, since they have a greater impact.</p>
<p><strong>1. Add a new key to the <code>encryption.keys</code> in you secrets</strong></p>
<div id="admonition-fail" class="admonition admonish-failure" role="note" aria-labelledby="admonition-fail-title">
<div class="admonition-title">
<div id="admonition-fail-title">
<p>Fail</p>
</div>
<a class="admonition-anchor-link" href="config/encryption.html#admonition-fail"></a>
</div>
<div>
<p>You <strong>must not remove</strong> a current key, before the migration has been done via the UI.<br />
If the old key is gone, the migration will fail.</p>
</div>
</div>
<p><strong>2. Generate a new key + id</strong></p>
<pre><code>echo "$(openssl rand -hex 4)/$(openssl rand -base64 32)"
</code></pre>
<p>The part before the first <code>/</code> is the <code>key_active</code> and the part afterwards is the key base64 encoded.
You might set the new <code>encryption.key_active</code> to the newly generated key ID.</p>
<p>Keep in mind, you need to ADD this to your existing keys and not just replace them! If you just replace them, almost
all things will break and fall apart.</p>
<p>The final format of the <code>encryption.keys</code> should look something like this, for instance:</p>
<pre><code class="language-toml">[encryption]
key = [
    "Wx1zrbLF/5vTaB7LdUSg1aTecmqHJOu2+RnU6zgTwNkDQU52Y3JM=",
    "6uf5QebA/9DsKMoq8A+Gn2WQrTcSpz5sg751yYs3IJlkw3dn0rII=",
]
</code></pre>
<p>In this example, if the first key should be the new active default, set your <code>key_active</code> to</p>
<pre><code class="language-toml">[encryption]
key_active = "Wx1zrbLF"
</code></pre>
<p><strong>3. Set the <code>key_active</code> to the ID of your newly generated key</strong></p>
<p>This will make sure, that all new encryptions will use the new key. If you do not care about removing the old keys,
because you maybe just want to rotate because its good practice, the secrets will migrate "by themselves" over time.
If Rauthy finds any secrets during its normal operation, that have been encrypted with an older key than the current
<code>key_active</code>, it will re-encrypt these secrets and update the values.<br />
This means, you may just stop at this point, if this is good enough for you.</p>
<p><strong>4. Migrate Keys</strong></p>
<p>If you however want to trigger a re-encryption of all existing secrets on purpose, there is a small tool in the
Admin UI which helps you with this.</p>
<p>Log in to the Admin UI and navigate to <code>Config</code> -&gt; <code>Encryption Keys</code>.<br />
You will see the currently recognized keys and the currently active ID.</p>
<p>You can then make sure, that the ID you want to migrate secrets to is selected and execute the migrations.
Please keep in mind, that if you have a lot of data, it might take a few seconds to perform this operation.
This will migrate all encrypted data for existing OIDC clients, all JWKs, and so on, with the new key.</p>
<p><strong>5. Remove old keys</strong></p>
<p>After a successful migration via the UI tool, you could remove old keys from the <code>keys</code> value, but it is not
recommended as long as you did not have a known data breach. Just keep them around for some time because of
encrypted cookies with older keys.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/encryption.html#admonition-caution"></a>
</div>
<div>
<p>All cookies are encrypted with the <code>key_active</code>. This means, if you remove something from the <code>keys</code> which has
been used to encrypt cookies, the user will be prompted to log in again, even if cookies have not expired yet.</p>
</div>
</div>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/encryption.html#admonition-note-1"></a>
</div>
<div>
<p>Rauthy uses ChaCha20Poly1305 for any encryption. AES GCM is not used on purpose, because it has some attack vectors if
its used without hardware acceleration.</p>
<p>Usually, devices these days all come with dedicated AES acceleration, even embedded ones. However, with VM's this is
often a different story and its not guaranteed, that you will have AES acceleration when you spin up a VM in some cloud.
Rauthy tries to be as secure as possible by default and therefore ChaCha20Poly1305 has a slight advantage over AES.</p>
</div>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="logging-and-auditing"><a class="header" href="#logging-and-auditing">Logging and Auditing</a></h1>
<p>Rauthy logs most things into the console, depending on the configuration of different log levels. In addition, more
important events can be sent to Matrix, Slack or via E-Mail. All of this is highly configurable and you should be able
to achieve whatever you need. All events are logged into the console as well with their configured level. This means,
if Rauthy fires an event of type <code>NewUser</code> with the level <code>info</code> and you have configured a log level of at least the
same, you will see the event in the console as well. So you could only use log aggregation and use existing tools
without configuring other targets and still catch everything.</p>
<h2 id="logging"><a class="header" href="#logging">Logging</a></h2>
<p>You can configure not only different levels for logging, but also different targets / situations.</p>
<h3 id="log_level"><a class="header" href="#log_level"><code>LOG_LEVEL</code></a></h3>
<p>The <code>LOG_LEVEL</code> variable configures the default logging in most situations. This defines the logging for instance
for logging information from different function runs or things that have been triggered.</p>
<pre><code class="language-toml">[logging]
# This is the log level for stdout logs
# Accepts: error, warn, info, debug, trace
#
# default: 'info'
# overwritten by: LOG_LEVEL
level = 'info'
</code></pre>
<h3 id="log_level_database"><a class="header" href="#log_level_database"><code>LOG_LEVEL_DATABASE</code></a></h3>
<p>The Hiqlite database logging is at the time of writing pretty verbose on purpose. The whole persistence layer with the
Raft cluster setup has been written from the ground up. The amount of logging will be reduced in later versions, when
the whole layer has been proven to be really solid, but for now you get more information just in case you need to debug
something.</p>
<p>You can reduce the default logging and for instance set it to <code>warn</code> or <code>error</code> only.</p>
<pre><code class="language-toml">[logging]
# The log level for the `Hiqlite` persistence layer.
# At the time of writing, only the cache will use `hiqlite`
#
# default: info
# overwritten by: LOG_LEVEL_DATABASE
level_database = 'info'
</code></pre>
<h3 id="log_level_access"><a class="header" href="#log_level_access"><code>LOG_LEVEL_ACCESS</code></a></h3>
<p>For changing the logging behavior for access logs to the API endpoints, you will need to set the <code>LOG_LEVEL_ACCESS</code>.
If you have access logging configured at your firewall or reverse proxy, you can disable the <code>LOG_LEVEL_ACCESS</code> fully
to reduce duplicated log outputs.</p>
<pre><code class="language-toml">[logging]
# This is a special config which allows the configuration of
# customized access logs. These logs will be logged with each
# request in addition to the normal LOG_LEVEL logs.
# The following values are valid:
# - `debug`
#   CAUTION: The Debug setting logs every information available
#   to the middleware which includes SENSITIVE HEADERS
#   DO NOT use the Debug level in a working production environment!
# - `verbose`
#   Verbose logging without headers - generates huge outputs
# - `basic`
#   Logs access to all endpoints apart from the Frontend ones which
#   all js, css, ...
# - `modifying`
#   Logs only requests to modifying endpoints and skips all GET
# - `off`
#
# default: 'modifying'
# overwritten by: LOG_LEVEL_ACCESS
level_access = 'modifying'
</code></pre>
<h3 id="log_fmt"><a class="header" href="#log_fmt"><code>LOG_FMT</code></a></h3>
<p>Rauthy can output logs as JSON data with the following variable:</p>
<pre><code class="language-toml">[logging]
# You can change the log output format to JSON, if you set:
# `log_fmt=json`.
# Keep in mind, that some logs will include escaped values,
# for instance when `Text` already logs a JSON in debug level.
# Some other logs like an Event for instance will be formatted
# as Text anyway. If you need to auto-parse events, please consider
# using an API token and listen ot them actively.
#
# default: text
# overwritten by: LOG_FMT
log_fmt = 'json'
</code></pre>
<h2 id="events"><a class="header" href="#events">Events</a></h2>
<p>Events are used for auditing and never miss anything. If something important happens, you usually need to inspect logs
to catch it, but why should you, if you did not notice any problems? This is where Rauthy Events are helping you out.
You need to set up basic configuration for Event targets and then you could customize the different levels.</p>
<h3 id="basic-setup"><a class="header" href="#basic-setup">Basic Setup</a></h3>
<p>Rauthy has the following Event targets built-in:</p>
<ul>
<li>E-Mail</li>
<li>Matrix</li>
<li>Slack</li>
</ul>
<p>You can see the full set of config option in the <code>[events]</code> in the <a href="config/config.html">Reference Config</a>.</p>
<h4 id="e-mail"><a class="header" href="#e-mail">E-Mail</a></h4>
<p>To be able to receive Events via E-Mail, you need to have set up an SMTP server and have a working connection. With
a working SMTP, you only need to set <code>events.email</code>, that's it.</p>
<pre><code class="language-toml">[events]
# The E-Mail address event notifications should be sent to.
#
# overwritten by: EVENT_EMAIL
email = 'admin@localhost'
</code></pre>
<h4 id="matrix"><a class="header" href="#matrix">Matrix</a></h4>
<p>Matrix is often deployed with home servers that may not even have real TLS certificates or if just running behind closed
doors, may use self-signed certificates. To make it work in all of these situations, you can configure quite a lot for
the connection to Matrix. In the end, you will only need to have some credentials and a room ID, so Rauthy knows where
it should post the events.</p>
<pre><code class="language-toml">[events]
# Matrix variables for event notifications.
# `matrix_user_id` and `matrix_room_id` are mandatory.
# Depending on your Matrix setup, additionally one of
# `matrix_access_token` or `matrix_user_password` is needed.
#
# If you log in to Matrix with User + Password, you may use
# `matrix_user_password`. If you log in via OIDC SSO (or just
# want to use a session token you can revoke), you should
# provide `matrix_access_token`.
# If both are given, the `matrix_access_token` will be preferred.
#
# If left empty, no messages will not be sent to Matrix.
# Format: `@&lt;user_id&gt;:&lt;server address&gt;`
#
# overwritten by: EVENT_MATRIX_USER_ID
matrix_user_id = ''
# Format: `!&lt;random string&gt;:&lt;server address&gt;`
# overwritten by: EVENT_MATRIX_ROOM_ID
matrix_room_id = ''
# overwritten by: EVENT_MATRIX_ACCESS_TOKEN
matrix_access_token = ''
# overwritten by: EVENT_MATRIX_USER_PASSWORD
matrix_user_password = ''
# URL of your Matrix server.
# default: https://matrix.org
# overwritten by: EVENT_MATRIX_SERVER_URL
matrix_server_url = 'https://matrix.org'

# Optional path to a PEM Root CA certificate file for the
# Matrix client.
#
# overwritten by: EVENT_MATRIX_ROOT_CA_PATH
matrix_root_ca_path = 'tls/root.cert.pem'

# May be set to disable the TLS validation for the Matrix
# client.
#
# default: false
# overwritten by: EVENT_MATRIX_DANGER_DISABLE_TLS_VALIDATION
matrix_danger_disable_tls_validation = false

# The default behavior is, that Rauthy will panic at startup
# if it cannot connect to a configured Matrix server. The
# reason is that event notifications cannot be dropped silently.
#
# However, if you use a self-hosted Matrix server which uses
# Rauthy as its OIDC provider and both instances went offline,
# you will have a chicken-and-egg problem:
# - Rauthy cannot connect to Matrix and will panic
# - Your Matrix server cannot connect to Rauthy and will panic
# To solve this issue, you can temporarily set this value to
# 'true' and revert back, after the system is online again.
#
# default: false
# overwritten by: EVENT_MATRIX_ERROR_NO_PANIC
matrix_error_no_panic = false
</code></pre>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="config/logging.html#admonition-hint"></a>
</div>
<div>
<p>I suggest that you create a separate room for these events. If you experience issues with Matrix encryption, you should
maybe disbale the encryption for the Events room. I came across some weird errors from Matrix in the past when I was
using a session token which has been created on another machine.</p>
</div>
</div>
<h4 id="slack"><a class="header" href="#slack">Slack</a></h4>
<p>To receive messages via Slack, you need to create a legacy webhook inside your Slack account. This is then the only
config variable you need to set:</p>
<pre><code class="language-toml">[events]
# The Webhook for Slack Notifications.
# If left empty, no messages will be sent to Slack.
#
# overwritten by: EVENT_SLACK_WEBHOOK
slack_webhook = ""
</code></pre>
<h4 id="custom-target"><a class="header" href="#custom-target">Custom Target</a></h4>
<p>If you need your events to be sent somewhere custom, you can always create an API key with <code>read</code> access for Events.
Then write a small app that simply listens to the events stream, which can process or just forwards the events to where
ever you need them.</p>
<ol>
<li>Log in to the Admin UI and create an API key with <code>read</code> access for events.</li>
</ol>
<p><img src="config/img/api_key_1.png" alt="api_key_events" /></p>
<ol start="2">
<li>Save the key, expand the new entry, navigate to <code>Secret</code> and <code>Generate New</code></li>
</ol>
<p><img src="config/img/api_key_2.png" alt="api_key_events" /></p>
<ol start="3">
<li>Copy the new Secret from the hidden input that will appear.
You will also see 2 prepared <code>curl</code> statements to test your new API key.
From the test, you should get an output similar to this:</li>
</ol>
<pre><code class="language-json">{
  "name": "events",
  "created": 1720428190,
  "expires": null,
  "access": [
    {
      "group": "Events",
      "access_rights": [
        "read"
      ]
    }
  ]
}
</code></pre>
<ol start="4">
<li>Listen to the events stream.
You need to add the API key in the <code>Authorization</code> header with the prefix <code>API-Key</code>, for instance like</li>
</ol>
<pre><code>Authorization: API-Key events$SUcpBxcPmfwH9z1Kb4ExOUYDSXpxOj9mFLadjuQ1049XaWzdWB328aa97k2nj21E
</code></pre>
<p>You can either periodically fetch via the <code>/auth/v1/events</code> endpoint (see Swagger documentation in the Admin UI), or
by listening to the <code>/auth/v1/events/stream</code>, which will be a Server Sent Events stream.</p>
<p>The events will be sent in JSON format and have the following content:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct Event {
    pub id: String,
    pub timestamp: i64,
    pub level: EventLevel,
    pub typ: EventType,
    pub ip: Option&lt;String&gt;,
    pub data: Option&lt;i64&gt;,
    pub text: Option&lt;String&gt;,
}

// the `EventLevel`s will be converted to lower case
enum EventLevel {
    Info,
    Notice,
    Warning,
    Critical,
}

enum EventType {
    InvalidLogins,
    IpBlacklisted,
    IpBlacklistRemoved,
    JwksRotated,
    NewUserRegistered,
    NewRauthyAdmin,
    NewRauthyVersion,
    PossibleBruteForce,
    RauthyStarted,
    RauthyHealthy,
    RauthyUnhealthy,
    SecretsMigrated,
    UserEmailChange,
    UserPasswordReset,
    Test,
}
<span class="boring">}</span></code></pre></pre>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/logging.html#admonition-note"></a>
</div>
<div>
<p>Keep in mind, that depending on proxies you might have in between, they might kill your connection to the events stream
after some timeout without messages. Your app should work around this and either re-connect on connection loss, or you
could fix it with a different proxy config or keep alive messages.</p>
</div>
</div>
<h3 id="testing"><a class="header" href="#testing">Testing</a></h3>
<p>You can test your Event handler / pipeline setup when you log in to the Admin UI and simply press the <code>Test</code> button
in the upper right corner in the Events sidebar. This will send a Test event to all configured targets and listeners
independent of any configured event level restrictions. Test events will always be sent as long as everything is working
properly.</p>
<h3 id="event_notify_level"><a class="header" href="#event_notify_level"><code>EVENT_NOTIFY_LEVEL</code></a></h3>
<p>You can set different levels for each target. By default, Only events with <code>warning</code> or higher are sent via E-Mail
while Matrix / Slack would receive Events with <code>notice</code> or higher. If you want a different behavior, you can get this:</p>
<pre><code class="language-toml">[events]
# The notification level for events. Works the same way as
# a logging level. For instance: 'notice' means send out a
# notifications for all events with the notice level or higher.
# Possible values:
# - info
# - notice
# - warning
# - critical
#
# default: 'warning'
# overwritten by: EVENT_NOTIFY_LEVEL_EMAIL
notify_level_email = 'warning'
# default: 'notice'
# overwritten by: EVENT_NOTIFY_LEVEL_MATRIX
notify_level_matrix = 'notice'
# default: 'notice'
# overwritten by: EVENT_NOTIFY_LEVEL_SLACK
notify_level_slack = 'notice'
</code></pre>
<h3 id="event-persistence"><a class="header" href="#event-persistence">Event Persistence</a></h3>
<p>Rauthys Admin UI has a component for inspecting Events from the past for analytical purposes. By default, events with
the level <code>info</code> or higher are persisted for <code>31</code> days. After this period, they will be removed from the database to
keep it clean.</p>
<p>You can configure both the level which should be persisted, for instance set "only persist Events with level warning
or higher" and the days how long they should be kept.</p>
<pre><code class="language-toml">[events]
# Define the level from which on events should be persisted
# inside the database. All events with a lower level will be
# lost, if there is no active event subscriber.
# Possible values:
# - info
# - notice
# - warning
# - critical
#
# default: 'info'
# overwritten by: EVENT_PERSIST_LEVEL
persist_level = 'info'

# Define the number of days when events should be cleaned
# up from the database.
#
# default: 30
# overwritten by: EVENT_CLEANUP_DAYS
cleanup_days = 30
</code></pre>
<h3 id="level_-values"><a class="header" href="#level_-values"><code>level_*</code> Values</a></h3>
<p>There are a lot of values starting with <code>level_*</code>. These can be used to configure the level for different kinds
of event being fired by Rauthy.</p>
<p>For instance, let's say you only want to receive events with a level of <code>warning</code> or higher, but you also want to
receive a notification when there are more than 7 failed logins from an IP. By default, 7 failed logins would trigger an
event with the level of <code>notice</code>. You can then set</p>
<pre><code class="language-toml">[events]
# default: notice
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_7
level_failed_logins_7 = 'notice'
</code></pre>
<p>to also receive these while still only receiving <code>warning</code> events.</p>
<p>The full list of these configurable levels is the following:</p>
<pre><code class="language-toml">[events]
# The level for the generated Event after a new user has
# been registered.
#
# default: info
# overwritten by: EVENT_LEVEL_NEW_USER
level_new_user = 'info'
# The level for the generated Event after a user has
# changed his E-Mail
#
# default: notice
# overwritten by: EVENT_LEVEL_USER_EMAIL_CHANGE
level_user_email_change = 'notice'
# The level for the generated Event after a user has
# reset its password
#
# default: notice
# overwritten by: EVENT_LEVEL_USER_PASSWORD_RESET
level_user_password_reset = 'notice'
# The level for the generated Event after a user has
# been given the 'rauthy_admin' role
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_ADMIN
level_rauthy_admin = 'notice'
# The level for the generated Event after a new App
# version has been found
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_VERSION
level_rauthy_version = 'notice'
# The level for the generated Event after the JWKS has
# been rotated
#
# default: notice
# overwritten by: EVENT_LEVEL_JWKS_ROTATE
level_jwks_rotate = 'notice'
# The level for the generated Event after DB secrets
# have been migrated to a new key
#
# default: notice
# overwritten by: EVENT_LEVEL_SECRETS_MIGRATED
level_secrets_migrated = 'notice'
# The level for the generated Event after a Rauthy
# instance has been started
#
# default: info
# overwritten by: EVENT_LEVEL_RAUTHY_START
level_rauthy_start = 'info'
# The level for the generated Event after a Rauthy
# entered a healthy state (again)
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_HEALTHY
level_rauthy_healthy = 'notice'
# The level for the generated Event after a Rauthy
# entered an unhealthy state
#
# default: critical
# overwritten by: EVENT_LEVEL_RAUTHY_UNHEALTHY
level_rauthy_unhealthy = 'critical'
# The level for the generated Event after an IP has
# been blacklisted
#
# default: warning+
# overwritten by: EVENT_LEVEL_IP_BLACKLISTED
level_ip_blacklisted = 'warning'
# The level for the generated Event after certain
# amounts of false logins from an IP
#
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_25
level_failed_logins_25 = 'critical'
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_20
level_failed_logins_20 = 'critical'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_15
level_failed_logins_15 = 'warning'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_10
level_failed_logins_10 = 'warning'
# default: notice
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_7
level_failed_logins_7 = 'notice'
# default: info
# overwritten by: EVENT_LEVEL_FAILED_LOGIN
level_failed_login = 'info'
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="backups"><a class="header" href="#backups">Backups</a></h1>
<h2 id="postgres"><a class="header" href="#postgres">Postgres</a></h2>
<p>If you are using Postgres as the main database, Rauthy does not do any backups.<br />
There are a lot of way better tools out there to handle this task.</p>
<h2 id="hiqlite"><a class="header" href="#hiqlite">Hiqlite</a></h2>
<p>If Rauthy is using Hiqlite, it does automatic backups, which can be configured with:</p>
<pre><code class="language-toml">[cluster]
# When the auto-backup task should run.
# Accepts cron syntax:
# "sec min hour day_of_month month day_of_week year"
#
# default: "0 30 2 * * * *"
# overwritten by: HQL_BACKUP_CRON
backup_cron = "0 30 2 * * * *"

# Backups older than the configured days will be cleaned up on S3
# after the backup cron job `backup_cron`.
#
# default: 30
# overwritten by: HQL_BACKUP_KEEP_DAYS
backup_keep_days = 30

# Backups older than the configured days will be cleaned up locally
# after each `Client::backup()` and the cron job `HQL_BACKUP_CRON`.
#
# default: 3
# overwritten by: HQL_BACKUP_KEEP_DAYS_LOCAL
backup_keep_days_local = 3
</code></pre>
<p>All these backups are written inside the pod / container into <code>data/state_machine/backups</code>.</p>
<p>This difference makes it possible, that you could add a second volume mount to the container.<br />
You then have the database itself on a different disk than the backups, which is the most simple and straight forward
approach to have a basic backup strategy. However, it is recommended to use S3 for backups, especially for HA
deployments.</p>
<h2 id="remote-backups-to-s3-storage"><a class="header" href="#remote-backups-to-s3-storage">Remote Backups to S3 Storage</a></h2>
<p>Hiqlite backups can be pushed to an S3 bucket after creation. This way, you can keep only very low amount of local
backups and older ones on cheaper object storage.</p>
<p>Rauthy has been tested against MinIO and Garage S3 storage and is working fine with both, so I expect and standard S3
API to just work out of the box. You need to provide an Access Key + Secret with write access to an existing bucket
and Rauthy will take care of the rest. All backups pushed to S3 will automatically encrypted with the currently active
<code>encryption.key_active</code> from the Rauthy config.</p>
<p>The configuration is done with the following values:</p>
<pre><code class="language-toml">[cluster]
# Access values for the S3 bucket where backups will be pushed to.
# overwritten by: HQL_S3_URL
s3_url = "https://s3.example.com"
# overwritten by: HQL_S3_BUCKET
s3_bucket = "my_bucket"
# overwritten by: HQL_S3_REGION
s3_region = "my_region"
# overwritten by: HQL_S3_PATH_STYLE
s3_path_style = true
# overwritten by: HQL_S3_KEY
s3_key = "my_key"
# overwritten by: HQL_S3_SECRET
s3_secret = "my_secret"
</code></pre>
<h2 id="disaster-recovery"><a class="header" href="#disaster-recovery">Disaster Recovery</a></h2>
<p>If you really lost all your data, you can easily restore automatically from the latest backup. This works with either a
local <code>file</code> backup or with an encrypted remote backup on <code>s3</code> storage (as long as you still have the <code>ENC_KEY_ACTIVE</code>
that has been used for the remote backup).<br />
This, again, works only for Hiqlite. When you are using Postgres, you should use Postgres native tooling like
<code>pgBackRest</code> which is way better at this.</p>
<p>The process is really simple:</p>
<ol>
<li>Have the cluster shut down. This is probably the case anyway, if you need to restore from a backup.</li>
<li>Provide a backup file name on S3 storage with the <code>HQL_BACKUP_RESTORE</code> ENV value with prefix <code>s3:</code> (encrypted), or a
file on disk (plain sqlite file) with the prefix <code>file:</code>.</li>
<li>Start up Rauthy</li>
<li>Check the logs and wait for the backup to be finished</li>
<li>After a successful restore, Rauthy will start its normal operation</li>
<li>Make sure to remove the <code>HQL_BACKUP_RESTORE</code> env value.</li>
</ol>
<div id="admonition-danger" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-title">
<div class="admonition-title">
<div id="admonition-danger-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="config/backup.html#admonition-danger"></a>
</div>
<div>
<p>After a successful restore, you MUST remove the env var again!<br />
If you don't do it, Rauthy will re-apply the same backup with each following restart over and over again.</p>
</div>
</div>
<p>You only need to set this single value:</p>
<pre><code class="language-toml"># If you ever need to restore from a backup, the process is simple.
# 1. Have the cluster shut down. This is probably the case anyway, if
#    you need to restore from a backup.
# 2. Provide the backup file name on S3 storage with the
#    HQL_BACKUP_RESTORE value.
# 3. Start up the cluster again.
# 4. After the restart, make sure to remove the HQL_BACKUP_RESTORE
#    env value.
HQL_BACKUP_RESTORE = ""
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="tls"><a class="header" href="#tls">TLS</a></h1>
<p>If you do have TLS certificates from another source already, skip directly to <a href="config/tls.html#config">Config</a>.</p>
<h2 id="generating-certificates"><a class="header" href="#generating-certificates">Generating Certificates</a></h2>
<p>You can either generate TLS certificates yourself and manage your own CA, or (for testing purposes) let Rauthy generate
self-signed certificates. Rauthys self-signed ones should only be used for testing, but they are generated somewhat
securely. Rauthy will create it's own CA and save it do the database, with the private key being encrypted with the
configured <code>encryption.key_active</code>. The CA will be valid for 10 years and re-used for signing the server certs in all
instances, even you do an HA deployment.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/tls.html#admonition-caution"></a>
</div>
<div>
<p>Some browsers like Firefox do not allow the registration of Passkeys when using self-signed TLS certificates. To be able
to do this during testing, you would need to add the generated CA certificate to your trust store.</p>
<p>In such a case, you will probably see an error like <code>Invalid Key</code> during registration. This happens in insecure contexts.</p>
</div>
</div>
<h3 id="automatic-self-signed-certificates"><a class="header" href="#automatic-self-signed-certificates">Automatic self-signed certificates</a></h3>
<p>Generating self-signed TLS certificates for the Rauthy HTTPS server is pretty straight forward. Keep in mind, that these
will only be generated for the HTTPS server and NOT for hiqlite internal cluster traffic, if you have an HA deployment.</p>
<p>To generate TLS certificates on startup, you only need to set <code>tls.generate_self_signed = true</code>:</p>
<pre><code class="language-toml">[tls]
# If set to `true`, Rauthy will generate self-signed TLS certs and copy
# them into `tls/self_signed_cert.pem` and `tls/self_signed_key.pem`.
# It will also IGNORE any `cert_path` / `key_path`.
#
# CAUTION: If set to `true`, it will delete existing files:
# - `tls/self_signed_cert.pem`
# - `tls/self_signed_key.pem`
#
# This should only be used for testing and never in production!
#
# default: false
# overwritten by: TLS_GENERATE_SELF_SIGNED
generate_self_signed = true
</code></pre>
<p>If you only care about testing certificates for the HTTPS server, you don't need to configure anything else at this
point.</p>
<h3 id="manually-managed-certificates"><a class="header" href="#manually-managed-certificates">Manually managed certificates</a></h3>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/tls.html#admonition-note"></a>
</div>
<div>
<p>We are using another project of mine called <a href="https://github.com/sebadob/nioca">Nioca</a> for an easy creation of a
fully functioning and production ready private Root Certificate Authority (CA).</p>
</div>
</div>
<p>I suggest to use <code>docker</code> for this task. Otherwise, you can use the <code>nioca</code> binary directly on any linux machine.
If you want a permanent way of generating certificates for yourself, take a look at Rauthys <code>justfile</code> and copy
and adjust the recipes <code>create-root-ca</code> and <code>create-end-entity-tls</code> to your liking.<br />
If you just want to get everything started quickly, follow these steps:</p>
<h4 id="create-an-alias-for-the-docker-command"><a class="header" href="#create-an-alias-for-the-docker-command">Create an alias for the <code>docker</code> command</a></h4>
<pre><code>alias nioca='docker run --rm -it -v ./:/ca -u $(id -u ${USER}):$(id -g ${USER}) ghcr.io/sebadob/nioca'
</code></pre>
<p>To see the full feature set for more customization than mentioned below:</p>
<pre><code>nioca x509 -h
</code></pre>
<h4 id="generate-full-certificate-chain"><a class="header" href="#generate-full-certificate-chain">Generate full certificate chain</a></h4>
<p>To make your browser happy, your need to have at least one <code>--alt-name-dns</code> for the URL of your application.
You can define as many of them as you like.</p>
<pre><code>nioca x509 \
    --cn 'Rauthy Default' \
    --o 'Rauthy OIDC' \
    --alt-name-dns localhost \
    --alt-name-dns rauthy.rauthy \
    --alt-name-dns rauthy.rauthy.svc.cluster.local \
    --usages-ext server-auth \
    --usages-ext client-auth \
    --stage full \
    --clean
</code></pre>
<p>You will be asked 6 times (yes, 6) for an at least 16 character password:</p>
<ul>
<li>The first 3 times, you need to provide the encryption password for your Root CA</li>
<li>The last 3 times, you should provide a different password for your Intermediate CA</li>
</ul>
<p>When everything was successful, you will have a new folder named <code>x509</code> with sub folders <code>root</code>, <code>intermediate</code>
and <code>end_entity</code> in your current one.</p>
<p>From these, you will need the following files:</p>
<pre><code>cp x509/intermediate/ca-chain.pem . &amp;&amp; \
cp x509/end_entity/$(cat x509/end_entity/serial)/cert-chain.pem . &amp;&amp; \
cp x509/end_entity/$(cat x509/end_entity/serial)/key.pem .
</code></pre>
<ul>
<li>You should have 3 files in <code>ls -l</code>:</li>
</ul>
<pre><code>ca-chain.pem
cert-chain.pem
key.pem
</code></pre>
<h2 id="config-2"><a class="header" href="#config-2">Config</a></h2>
<p>The <a href="config/../config/config.html">reference config</a> contains a <code>TLS</code> section with all the values you can set.</p>
<p>For this example, we will be using the same certificates for both the internal cache mTLS connections and the
public facing HTTPS server.</p>
<h3 id="hiqlite-1"><a class="header" href="#hiqlite-1">Hiqlite</a></h3>
<p>Hiqlite can run the whole database layer, and it will always take care of caching. It can be configured to use TLS
internally, if you provide certificates. Simply provide the following values from the <code>TLS</code> section in the reference
config:</p>
<pre><code class="language-toml">[cluster]
# If given, these keys / certificates will be used to establish
# TLS connections between nodes.
#
# values are optional, overwritten by: HQL_TLS_{RAFT|API}_{KEY|CERT}
# overwritten by: HQL_TLS_RAFT_KEY
tls_raft_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_CERT
tls_raft_cert = "tls/tls.crt"
#tls_raft_danger_tls_no_verify = true

# overwritten by: HQL_TLS_API_KEY
tls_api_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_KEY
tls_api_cert = "tls/tls.crt"
#tls_api_danger_tls_no_verify = true
</code></pre>
<p>There is no problem using the same certificates for both networks, but you can optionally even separate them if you need
to. You could even re-use the Server TLS, if your DNS setup allows for this.</p>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/tls.html#admonition-note-1"></a>
</div>
<div>
<p>At the time of writing, it does not accept a custom Root CA yet. In this case you have to set the
<code>*_danger_tls_no_verify</code> to <code>true</code></p>
</div>
</div>
<h3 id="rauthy-server--api"><a class="header" href="#rauthy-server--api">Rauthy Server / API</a></h3>
<p>By default, rauthy will expect a certificate and a key file in <code>/app/tls/tls.key</code> and <code>/app/tls/tls.crt</code>, which
is the default naming for a Kubernetes TLS secret. The expected format is PEM, but you could provide the key in DER
format too, if you rename the file-ending to <code>*.der</code>.</p>
<pre><code class="language-toml">[tls]
## UI + API TLS

# Overwrite the path to the TLS certificate file in PEM
# format for rauthy
#
#default: 'tls/tls.crt'
# overwritten by: TLS_CERT
cert_path = 'tls/cert-chain.pem'

# Overwrite the path to the TLS private key file in PEM
# format for rauthy. If the path / filename ends with '.der',
# rauthy will parse it as DER, otherwise as PEM.
#
# default: 'tls/tls.key'
# overwritten by: TLS_KEY
key_path = 'tls/key.pem'
</code></pre>
<h3 id="kubernetes-1"><a class="header" href="#kubernetes-1">Kubernetes</a></h3>
<p>If you did not follow the above procedure to generate the CA and certificates, you may need to rename the files in the
following command, to create the Kubernetes secrets.</p>
<p><strong>Secrets - Rauthy Server / API</strong></p>
<pre><code>kubectl -n rauthy create secret tls rauthy-tls --key="key.pem" --cert="cert-chain.pem"
</code></pre>
<p><strong>Secrets - <code>hiqlite</code> cache</strong></p>
<pre><code>kubectl -n rauthy create secret tls hiqlite-tls --key="key.pem" --cert="cert-chain.pem"
</code></pre>
<div id="admonition-note-2" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-2-title">
<div class="admonition-title">
<div id="admonition-note-2-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/tls.html#admonition-note-2"></a>
</div>
<div>
<p>We create the <code>hiqlite-tls</code> here with the exact same values. You could of course either use different certificates, or
not create a separate secret at all and just re-use the Rauthy TLS certificates, if you DNS setup allows for proper
validation in this case.</p>
</div>
</div>
<h4 id="config-adjustments---rest-api"><a class="header" href="#config-adjustments---rest-api">Config Adjustments - REST API</a></h4>
<p>We need to configure the newly created Kubernetes secrets in the <code>sts.yaml</code> from the
<a href="config/../getting_started/k8s.html#create-and-apply-the-stateful-set">Kubernetes</a> setup.</p>
<ol>
<li>In the <code>spec.template.spec.volumes</code> section, we need to mount the volumes from secrets:</li>
</ol>
<p><strong>REST API</strong>:</p>
<pre><code class="language-yaml">- name: rauthy-tls
  secret:
    secretName: rauthy-tls
</code></pre>
<p><strong><code>hiqlite</code> cache</strong>:</p>
<pre><code class="language-yaml">- name: hiqlite-tls
  secret:
    secretName: hiqlite-tls
</code></pre>
<ol start="2">
<li>In the <code>spec.template.spec.containers.[rauthy].volumeMounts</code> section, add::</li>
</ol>
<p><strong>REST API</strong>:</p>
<pre><code class="language-yaml">- mountPath: /app/tls/
  name: rauthy-tls
  readOnly: true
</code></pre>
<p><strong><code>hiqlite</code> cache</strong>:</p>
<pre><code class="language-yaml">- mountPath: /app/tls/hiqlite/
  name: hiqlite-tls
  readOnly: true
</code></pre>
<p>After having modified the config from above and the <code>sts.yaml</code> now, just apply both:</p>
<pre><code>kubectl apply -f config.yaml
kubectl apply -f sts.yaml
</code></pre>
<p>The <code>rauthy</code> pods should restart now and TLS is configured.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="sessions"><a class="header" href="#sessions">Sessions</a></h1>
<p>When you log in to anything, be it your account dashboard, the admin UI, or a downstream application, you will get a
Rauthy session. This is independent of any client / application login. These sessions are used to authorize against
Rauthy only for things like account management. Depending on your configuration, users may be logged in to a downstream
client immediately, if they already have a still valid session.</p>
<p>You can configure quite a lot like session timeouts and so on, but the default are safe. However, there is one
really important thing:</p>
<p><strong>You need to make sure, that Rauthy can extract the connecting clients IP properly. This is
very important for failed login counters, login delays, blacklisting, and so on.</strong></p>
<p>If your instance is exposed directly, in most situations the IP extractions works just fine. This may change though
when running behind a reverse proxy or a CDN.</p>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="config/sessions.html#admonition-hint"></a>
</div>
<div>
<p>To check which IP Rauthy will extract for your requests, you don't to search through logs. You can use the whoami
endpoint. This is unauthenticated and will just return your current IP from the request headers. If the returned
IP is correct, your setup is fine.</p>
<p>The endpoint is reachable via: <code>/auth/v1/whoami</code></p>
</div>
</div>
<h2 id="running-behind-a-reverse-proxy"><a class="header" href="#running-behind-a-reverse-proxy">Running behind a reverse proxy</a></h2>
<p>If you are running behind a reverse proxy, you need to set at least 2 config variable properly.<br />
First, you need to set</p>
<pre><code class="language-toml">[server]
# When rauthy is running behind a reverse proxy, set to true
#
# default: false
# overwritten by: PROXY_MODE
proxy_mode = false
</code></pre>
<p>Secondly, you need to tell Rauthy which proxy source IP's it can trust. This is important, because when behind a reverse
proxy, Rauthy will only see the IP of the proxy itself by default, which would be the same for each client connecting
though it. However, a reverse proxy adds headers which contain the clients real IP, like e.g. the <code>X-FORWARED-FOR</code>
header and maybe others (depending on the proxy).</p>
<p>These headers can be spoofed from an attacker, if the source IP is not validated. This is what Rauthy needs you to set
the trusted proxies config for:</p>
<pre><code class="language-toml">[server]
# A list of trusted proxy CIDRs. When `proxy_mode = true`
# or `peer_ip_header_name` is set, these are mandatory to
# be able to extract the real client IP properly and safely
# to prevent IP header spoofing. All requests witha
# different source will be blocked.
#
# default: []
# overwritten by: TRUSTED_PROXIES - single String, \n separated values
trusted_proxies = ['192.168.0.1/32']
</code></pre>
<p>The more you can narrow down the CIDR for your reverse proxy, the better. For instance if you know your proxy is your
firewall at the same time, which always will have the IP <code>192.168.0.1</code>, then add the <code>/32</code> subnet to it. If you are
running in a more dynamic environment like Docker or Kubernetes, where your proxy could get an IP dynamically from an
internal pool, you need to add all the possible IPs as trustworthy.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/sessions.html#admonition-caution"></a>
</div>
<div>
<p>When Rauthy is running in proxy mode, it will block every incoming request that does not match the
<code>trusted_proxies</code> IP pool. This means if you have internal tooling set up like health checks, monitoring or
metrics, which do not connect via the proxy, you need to add these source IPs to the <code>trusted_proxies</code> list.</p>
</div>
</div>
<h3 id="running-behind-a-cdn"><a class="header" href="#running-behind-a-cdn">Running behind a CDN</a></h3>
<p>If you are running behind a CDN which proxies your requests like for instance cloudflare, you have a reverse proxy
setup again, just so that cloudflare is (another) reverse proxy for you. This means you need to set up the above
configuration at least.</p>
<p>In addition, you would maybe end up seeing the CDN proxy IP when you do a <code>GET /auth/v1/whoami</code>. If this is the case,
There is an additional variable you can set:</p>
<pre><code class="language-toml">[server]
# Can be set to extract the remote client peer IP from a custom
# header name instead of the default mechanisms. This is needed
# when you are running behind a proxy which does not set the
# `X-REAL-IP` or `X-FORWARDED-FOR` headers correctly, or for
# instance when you proxy your requests through a CDN like
# Cloudflare, which adds custom headers in this case. For instance,
# if your requests are proxied through cloudflare, your would set
# `CF-Connecting-IP`.
#
# overwritten by: PEER_IP_HEADER_NAME
peer_ip_header_name = 'CF-Connecting-IP'
</code></pre>
<p>The CDN usually adds some other headers than the default <code>X-FORWARED-FOR</code> headers, like in this example
<code>CF-Connecting-IP</code> to the request. If this is the case, you can tell Rauthy to always check for this header first and
only use the other methods as fallback, if this does not exist.</p>
<h3 id="session-peer-ip-binding"><a class="header" href="#session-peer-ip-binding">Session peer IP binding</a></h3>
<p>You most probably do not need to care about this configuration, but depending on your application you may want to
disable it.</p>
<p>Whenever you get a session from Rauthy and you authenticate successfully, your current IP will be extracted and
persisted. By default, Rauthy will check your origin IP and compare it to the one you had when creating the session
with each single request. If your IP does not match the original one, the session will be ignored and the request will
be treated as being unauthenticated.</p>
<p>This prevents scenarios where an attacker would be able to steal session data from your machine, copy the information
and use it on their own. This means even if you would send your session cookie and CSRF token to someone, they would not
be able to use it, as long as the requests are not coming from the exact same source IP.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/sessions.html#admonition-note"></a>
</div>
<div>
<p>Rauthy has lots of mechanisms in place to prevent things like cookie stealing, session takeover, and so on , but it
can't do anything about it, when the client's OS itself is infected. All these mechanisms add up to the defense in
depth, but at the end of the day, when the clients machine itself is infected, there is not much any application can do
about it. There just is no silver bullet.</p>
</div>
</div>
<p>This defense is a really nice thing, but it may annoy your users, depending on where your deployed Rauthy, because
this also means that each time when a client's IP changes, like for instance when you are in a mobile network or in
a WIFI and often reconnect, your session will not be accepted. With as passkey added to your account, the login will
take only seconds and another touch on the device, but you may still want to disable it. In this case, here is the
configuration:</p>
<pre><code class="language-toml">[access]
# If set to 'true', this will validate the remote peer IP address with
# each request and compare it with the IP which was used during the
# initial session creation / login. If the IP is different, the session
# will be rejected.
#
# This is a security hardening and prevents stolen access credentials,
# for instance if an attacker might have copied the encrypted session
# cookie and the XSRF token from the local storage from a user.
# However, this event is really unlikely, since it may only happen if
# an attacker has direct access to the machine itself.
#
# If your users are using mobile networks and get new IP addresses all
# the time, this means they have to do a new login each time. This is
# no big deal at all with Webauthn / FIDO keys anyway and should not
# be a reason to deactivate this feature.
#
# CAUTION: If you are running behind a reverse proxy which does not
# provide the X-FORWARDED-FOR header correctly, or you have the
# `proxy_mode` in this config disabled, this feature will not work.
# You can validate the IPs for each session in the Admin UI. If these
# are correct, your setup is okay.
#
# default: true
# overwritten by: SESSION_VALIDATE_IP
session_validate_ip = true
</code></pre>
<h3 id="lifetimes-and-timeouts"><a class="header" href="#lifetimes-and-timeouts">Lifetimes and Timeouts</a></h3>
<p>The default session lifetimes and timeouts are pretty secure, but you may find them to be too strict. You can adjust
them with the following config variables:</p>
<pre><code class="language-toml">[lifetimes]
# Session lifetime in seconds - the session can not be extended
# beyond this time and a new login will be forced. This is the
# session for the authorization code flow.
#
# default: 14400
# overwritten by: SESSION_LIFETIME
session_lifetime = 14400

# If 'true', a 2FA / MFA check will be done with each automatic
# token generation, even with an active session, which kind of
# makes the session useless with Webauthn enabled, but provides
# maximum amount of security. If 'false', the user will not get
# an MFA prompt with an active session at the authorization endpoint.
#
# default: false
# overwritten by: SESSION_RENEW_MFA
session_renew_mfa = false

# Session timeout in seconds. When a new token / login is requested
# before this timeout hits the limit, the user will be authenticated
# without prompting for the credentials again.
#
# This is the value which can extend the session, until it hits its
# maximum lifetime set with session_lifetime.
#
# default: 5400
# overwritten by: SESSION_TIMEOUT
session_timeout = 5400
</code></pre>
<h2 id="security"><a class="header" href="#security">Security</a></h2>
<p>You usually don't need to configure anything about session security or CSRF protection, all of it happens automatically.
This section is more informational about what Rauthy does in this case.</p>
<h3 id="session-cookies"><a class="header" href="#session-cookies">Session Cookies</a></h3>
<p>Rauthy stores sessions as encrypted cookies. Depending on the situation, configuration and the users account, it will
set multiple cookies inside your browser for different purposes like if you are allowed to do a direct session refresh
with an MFA account for instance.</p>
<p>Apart from the locale preference for the UI, each cookie is stored encrypted. This makes sure, that you can't tamper
with the data. The cookies are stored as host only cookies with the most secure settings in today's browsers by default.
This means they are host only and use the <code>__Host-</code> prefix to tell the browser to do additional checks. The <code>SameSite</code>
attribute is set to <code>Lax</code> for all of them.</p>
<p>Apart from local testing, you should never get in the situation where you want to disable the default secure
cookie settings. But if you really need to (and you know what you are doing), you have the following option:</p>
<pre><code class="language-toml">[access]
# You can set different security levels for Rauthy's cookies. The
# safest option would be 'host', but may not be desirable when you
# host an application on the same origin behind a reverse proxy.
# In this case you might want to restrict to 'secure', which will
# then take the `cookie_set_path` from below into account. The last
# option is 'danger-insecure' which really should never be used
# unless you are just testing on localhost.
#
# default: host
# overwritten by: COOKIE_MODE
cookie_mode = 'host'

# If set to 'true', Rauthy will bind the cookie to the `/auth`
# path. You may want to change this only for very specific reasons
# and if you are in such a situation, where you need this, you will
# know it. Otherwise, don't change this value.
#
# default: true
# overwritten by: COOKIE_SET_PATH
cookie_set_path = true
</code></pre>
<h3 id="csrf-protection"><a class="header" href="#csrf-protection">CSRF Protection</a></h3>
<p>CSRF protection happens in multiple ways:</p>
<ul>
<li><code>CORS</code> / <code>Origin</code> headers</li>
<li>classic synchronizer token pattern</li>
<li><code>Sec-</code> headers checks</li>
</ul>
<p>In today's browsers, you could use the <code>Sec-</code> headers only and be safe, or actually even only stick to the secure
Cookie settings we have, and call it a day. The additional checks Rauthy does in this case are there to catch unusual
situations, where someone maybe uses an older browser or one which has a security issue. All of these techniques are
defenses in depth.</p>
<p>The synchronizer token pattern stores the additional CSRF token in local storage. Yes, this is not "secure" in a way
that a malicious browser extension can read it, but it could read the DOM as well, which means it could also just
read a meta tag or extract it from a hidden form field. The backend expects the CSRF token to be added as a header
with each non-<code>GET</code> request.</p>
<p>A new token will be created when you get a fresh session. Generating a new token after each request would improve the
security but badly hurt the UX, because the browsers back button would simply not work anymore in most cases. In a
perfect world where all users only use modern browsers that fully respect today's cookie settings, we would not even
need this token, so a new token with each new session is fine.</p>
<p>The <code>Sec-</code> headers middleware has been added recently to the mix. Desktop browsers do add these headers since ~3 years
by now, but they only have been added pretty recently to mobile browsers as well. This middleware on its own would be
a full CSRF protection even without additional cookie settings or a synchronizer token, but these headers are just
way too fresh on mobile browsers to only rely on them right now.</p>
<p>The <code>Sec-</code> middleware is pretty new to Rauthy, so it might be too restrictive in some situations where I forgot to add
an exception for. By default, it blocks any non-user initiated or navigating cross-origin request and I added exceptions
for routes, which should be available cross-origin. If you experience issues with it, you might want to disable it and
set it to warn-only mode. Please <a href="https://github.com/sebadob/rauthy/issues">open an issue</a> about this though so it can
be fixed, if it makes sense, because this option will probably be removed in a future version:</p>
<pre><code class="language-toml">[access]
# If set to true, a violation inside the CSRF protection middleware
# based on Sec-* headers will block invalid requests. Usually you
# always want this enabled. You may only set it to false during the
# first testing phase if you experience any issues with an already
# existing Rauthy deployment. In future releases, it will not be
# possible to disable these blocks.
#
# default: true
# overwritten by: SEC_HEADER_BLOCK
sec_header_block = true
</code></pre>
<div id="admonition-danger" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-title">
<div class="admonition-title">
<div id="admonition-danger-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="config/sessions.html#admonition-danger"></a>
</div>
<div>
<p>Only change any of the above mentioned session security settings if you really know what you are doing and if you have
a good reason to do so.</p>
</div>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="user-registration"><a class="header" href="#user-registration">User Registration</a></h1>
<p>By default, new users can only be added by an admin. This is safe and secure, and your database cannot ever be spammed.
However, you might want users to register themselves for whatever reason. In that case, follow the next step.</p>
<h2 id="open-registration"><a class="header" href="#open-registration">Open Registration</a></h2>
<p>To open the registration to anyone, just set</p>
<pre><code class="language-toml">[user_registration]
# If the User Registration endpoint should be accessible by anyone.
# If not, an admin must create each new user.
#
# default: false
# overwritten by: OPEN_USER_REG
enable = true
</code></pre>
<p>This will open the registration endpoint and make it accessible without upfront authentication.<br />
You will now see a new button at the page root which directs you to the very simple registration form.</p>
<p>After successful registration, the user will receive an E-Mail with an activation link.
Clicking on this link will direct the user on a page, where a new password or passkey can be set.
At the same time, the account will be activated and the E-Mail will be validated as well of course.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/user_reg.html#admonition-caution"></a>
</div>
<div>
<p>You must have configured your E-Mail sending beforehand. Otherwise the registration process will not work.
You need to configure the following values to be able to send out E-Mails:</p>
<ul>
<li>SMTP_URL</li>
<li>SMTP_USERNAME</li>
<li>SMTP_PASSWORD</li>
</ul>
</div>
</div>
<h2 id="captcha--proof-of-work"><a class="header" href="#captcha--proof-of-work">Captcha / Proof of Work</a></h2>
<p>The registration form uses a <em>Proof of Work (PoW)</em> behind the scenes. This is basically an invisible captcha
without the user solving weird image puzzles that sometimes even humans cannot solve. It is done with the help of a
tiny crate I wrote myself as well.</p>
<p>This will of course not prevent real humans from registering fake accounts, but until now, I never had issues with any
bots, so it does what it should while providing a way better UX than any traditional captcha challenge.</p>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="config/user_reg.html#admonition-info"></a>
</div>
<div>
<p>If you are interested in how it works, take a look at <a href="https://github.com/sebadob/spow">spow</a>.</p>
</div>
</div>
<h2 id="restricted-registration"><a class="header" href="#restricted-registration">Restricted Registration</a></h2>
<p>You may want your users to register themselves, but at the same time restrict the E-Mail domains they are using.
For instance, when you deploy Rauthy at your company for all internal applications, you may only want users to
register with their work E-Mail:</p>
<pre><code class="language-toml">[user_registration]
# Can be used when 'open_user_reg = true' to restrict the domains for
# a registration. For instance, set it to
# 'user_reg_domain_restriction = gmail.com' to allow only
# registrations with 'user@gmail.com'.
#
# overwritten by: USER_REG_DOMAIN_RESTRICTION
domain_restriction = 'my-domain.com'
</code></pre>
<h2 id="domain-blacklisting"><a class="header" href="#domain-blacklisting">Domain Blacklisting</a></h2>
<p>If you have opened your registration to anyone, you will get into the situation at some point, where evil people will
create accounts only for checking out your security and trying to break in, execute XSS, and so on. These are often
real people, which means any Captcha or PoW will not prevent them from registering of course.</p>
<p>The best thing you can do in that case, if your deployment allows this, is Geoblocking certain regions at your
firewall / reverse proxy level. Attackers would then switch to origins in a country you allow, but this is usually
more costly for them and more hassle, so it is a good first line of defense. At the same time, it reduces the visibility
to those regions, and you might not get targeted in the first place because bots cannot scan you from there.</p>
<p>When such attacks happen, there is no magic rule or setting to defend against them. This is really individual each time
and depends on a lot of things out of the scope of this documentation.</p>
<p>Nevertheless, Rauthy provides a small feature that might help you here: <strong>E-Mail Domain Blacklisting</strong></p>
<p>Let's say you can't Geoblock or you found out, that specific E-Mail providers are being used during these attacks.
If you have such a list of evil providers, you can blacklist and exclude them from the open registration. Existing users
will keep working and an Admin can still create users with these domains. They just cannot be used for self-registration
anymore.</p>
<p>You have the following config option:</p>
<pre><code class="language-toml">[user_registration]
# Can be used when 'open_user_reg = true' to restrict the domains for
# a registration. For instance, set it to
# 'user_reg_domain_restriction = gmail.com' to allow only
# registrations with 'user@gmail.com'.
#
# overwritten by: USER_REG_DOMAIN_RESTRICTION
#domain_restriction = 'my-domain.com'

# If `open_user_reg = true`, you can blacklist certain domains on
# the open registration endpoint.
#
# default: []
# overwritten by: USER_REG_DOMAIN_BLACKLIST - single String, \n separated values
domain_blacklist = [
    'example.com',
    'evil.net',
]
</code></pre>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/user_reg.html#admonition-note"></a>
</div>
<div>
<p>If you get targeted by something like fake accounts for testing your security and so on, don't panic.
These attacks usually stop after 1-2 weeks most often. When attackers did not find a way in, they loose interest.</p>
</div>
</div>
<h2 id="downstream-application-integration"><a class="header" href="#downstream-application-integration">Downstream Application Integration</a></h2>
<p>You can integrate the registration into a downstream application on 2 different ways.<br />
Either use the existing one with redirect hints for improved UX, or fully build your own registration frontend.</p>
<h3 id="redirect-hints"><a class="header" href="#redirect-hints">Redirect Hints</a></h3>
<p>Let's say you have an application that uses Rauthy for user management. You want to improve the registration flow
and your users being redirected back to your downstream app after registration / password set instead of them
"being stuck" on Rauthy's UI, which would be the default flow.</p>
<p>Your app can show a link to the existing registration form with an appended query param to control redirects.
When you append</p>
<pre><code>?redirect_uri=https%3A%2F%2Fgithub.com
</code></pre>
<p>to the link, so you end up with for instance</p>
<pre><code>https://iam.example.com/auth/v1/users/register?redirect_uri=https%3A%2F%2Fgithub.com
</code></pre>
<p>The following things will happen:</p>
<ol>
<li>After a successful registration, the user will be redirected back to the given <code>redirect_uri</code>.</li>
<li>After the password / passkey has been set using the E-Mail link, instead of being redirected to the Rauthy
account dashboard, the user will be redirected to the same URI again.</li>
</ol>
<p>This makes it possible to use Rauthy as your upstream provider without the user really needing to interact with or
know about it in detail, which again leads to less confusion.</p>
<p>By default, the allowed <code>redirect_uri</code>s are restricted to all existing <code>client_uri</code>s in the database. They will be
compared via <code>client_uri.startsWith(redirect_uri)</code>. If you want to opt-out of the additional redirect_uri checks and
configure and open redirect to allow just anything, you can do so:</p>
<pre><code class="language-toml">[user_registration]
# If set to `true`, any validation of the `redirect_uri` provided
# during a user registration will be disabled. Clients can use
# this feature to redirect the user back to their application
# after a successful registration, so instead of ending up in the
# user dashboard, they come back to the client app that initiated
# the registration.
#
# The given `redirect_uri` will be compared against all registered
# `client_uri`s and will throw an error, if there is no match.
# However, this check will prevent ephemeral clients from using
# this feature. Only if you need it in combination with ephemeral
# clients, you should set this option to `true`. Otherwise, it is
# advised to set the correct Client URI in the admin UI. The
# `redirect_uri` will be allowed if it starts with any registered
# `client_uri`.
#
# default: false
# overwritten by: USER_REG_OPEN_REDIRECT
allow_open_redirect = false
</code></pre>
<h3 id="custom-frontend"><a class="header" href="#custom-frontend">Custom Frontend</a></h3>
<p>Depending on your application, you may want to create your own frontend for the registration. For speed and efficiency
reasons, Rauthy does not allow you to overwrite the existing templates, but you can host your own UI of course.</p>
<p>The registration page is super simple and you can take a look at what it does here:
<a href="https://github.com/sebadob/rauthy/blob/main/frontend/src/routes/users/register/%2Bpage.svelte">page.svelte</a></p>
<p>The registration endpoint allows CORS requests. The only thing you need to care about is the PoW calculation.</p>
<ol>
<li>Accept the input from the user via any form inputs</li>
<li>When the user clicks submit, fetch a new PoW from Rauthy via <code>POST /auth/v1/pow</code></li>
<li>Solve the PoW using <a href="https://github.com/sebadob/spow">spow</a></li>
<li>As soon as the PoW is solved, do a <code>POST /auth/v1/users/register</code> with the payload</li>
</ol>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct NewUserRegistrationRequest {
    #[validate(email)]
    email: String,
    /// Validation: `[a-zA-Z0-9À-ÿ-'\\s]{1,32}`
    #[validate(regex(path = "*RE_USER_NAME", code = "[a-zA-Z0-9À-ɏ-'\\s]{1,32}"))]
    family_name: String,
    /// Validation: `[a-zA-Z0-9À-ÿ-'\\s]{1,32}`
    #[validate(regex(path = "*RE_USER_NAME", code = "[a-zA-Z0-9À-ɏ-'\\s]{1,32}"))]
    given_name: String,
    /// Validation: `[a-zA-Z0-9,.:/_\-&amp;?=~#!$'()*+%]+`
    #[validate(regex(path = "*RE_URI", code = "[a-zA-Z0-9,.:/_\\-&amp;?=~#!$'()*+%]+"))]
    pow: String,
    /// Validation: `[a-zA-Z0-9,.:/_\-&amp;?=~#!$'()*+%]+`
    #[validate(regex(path = "*RE_URI", code = "[a-zA-Z0-9,.:/_\\-&amp;?=~#!$'()*+%]+"))]
    redirect_uri: Option&lt;String&gt;,
}
<span class="boring">}</span></code></pre></pre>
<div id="admonition-caution-1" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-1-title">
<div class="admonition-title">
<div id="admonition-caution-1-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/user_reg.html#admonition-caution-1"></a>
</div>
<div>
<p>Do not fetch and solve a PoW when the user has not submitted the form yet!</p>
<p>The PoWs have a very short lifetime by default to prevent them from being used multiple times. Rauthy has additional
re-use prevention, but a POST to get a PoW will modify the backend state. This is unnecessary if the user decides to not
submit the form at all.</p>
<p>You can configure PoWs with <code>pow.difficulty</code> and <code>pow.exp</code>.<br />
Keep in mind, that the <code>pow.exp</code> should be a high as necessary but always as low as possible.</p>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="high-availability"><a class="header" href="#high-availability">High Availability</a></h1>
<p>Rauthy is capable of running in a High Availability Mode (HA).</p>
<p>Some values, like authentication codes for instance, do live in the cache only. Because of this, all instances create
and share a single HA cache layer, which means at the same time, that you cannot just scale up the replicas infinitely
without adjusting the config. The optimal amount of replicas for a HA mode would be 3, or if you need even higher
resilience 5. More replicas should work just fine, but at some point, the write throughput will degrade.</p>
<p>The Cache layer uses <a href="https://github.com/sebadob/hiqlite">Hiqlite</a>. It uses the Raft algorithm under the hood to achieve
consistency.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/ha.html#admonition-caution"></a>
</div>
<div>
<p>Even though everything is authenticated, you should not expose the
Hiqlite ports to the public, if not really necessary for some reason. You configure these ports with the <code>cluster.nodes</code>
config value.</p>
</div>
</div>
<h2 id="configuration"><a class="header" href="#configuration">Configuration</a></h2>
<p>Earlier versions of Rauthy have been using <a href="https://github.com/sebadob/redhac">redhac</a> for the HA cache layer. While
<code>redhac</code> was working fine, it had a few design issues I wanted to get rid of. Since <code>v0.26.0</code>, Rauthy uses the
above-mentioned <a href="https://github.com/sebadob/hiqlite">Hiqlite</a> instead. You only need to configure a few variables.</p>
<div id="admonition-caution-1" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-1-title">
<div class="admonition-title">
<div id="admonition-caution-1-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/ha.html#admonition-caution-1"></a>
</div>
<div>
<p>Even when using Postgres as your DB of choice, in HA deployments, you should always provide persistent volumes to your
Rauthy Pods. One reason is that it smoothes out rolling releases, because even though you can keep the Cache Raft
in-memory only because it can handle these situations, even when using Postgres, Rauthy will create at least an empty
SQLite cache layer. If this loses state between restarts and they happen too fast for instance, this can end up in
crashes.</p>
<p>It is also very recommended to persist the cache on disk anyway (which is the default), to keep things like not yet used
Auth Codes between restarts.</p>
</div>
</div>
<h3 id="node_id"><a class="header" href="#node_id"><code>node_id</code></a></h3>
<p>The <code>cluster.node_id</code> is mandatory, even for a single replica deployment with only a single node in <code>cluster.nodes</code>.
If you deploy Rauthy as a StatefulSet inside Kubernetes, you can ignore this value and just set <code>HQL_NODE_ID_FROM</code>
below. If you deploy anywhere else, or you are not using a StatefulSet, you need to set the <code>node_id_from</code> to tell
Rauthy
which node of the Raft cluster it should be.</p>
<pre><code class="language-toml">[cluster]
# The node id must exist in the nodes and there must always be
# at least a node with ID 1
# Will be ignored if `node_id_from = k8s`
#
# At least `node_id_from` or `node_id` are required.
#
# default: 0 (invalid)
# overwritten by: HQL_NODE_ID
node_id = 1
</code></pre>
<h3 id="node_id_from"><a class="header" href="#node_id_from"><code>node_id_from</code></a></h3>
<p>If you deploy to Kubernetes as a StatefulSet, you should ignore the <code>cluster.node_id</code> and just set
<code>cluster.node_id_from = "k8s"</code>. This will parse the correct NodeID from the Pod hostname, so you don't have to worry
about it.</p>
<pre><code class="language-toml">
[cluster]
# Can be set to 'k8s' to try to split off the node id from the hostname
# when Hiqlite is running as a StatefulSet inside Kubernetes.
#
# default: unset
# overwritten by: HQL_NODE_ID_FROM
node_id_from = "k8s"
</code></pre>
<h3 id="nodes"><a class="header" href="#nodes"><code>nodes</code></a></h3>
<p>This value defines the Raft members. It must be given even if you just deploy a single instance. The description from
the reference config should be clear enough:</p>
<pre><code class="language-toml">[cluster]
# All cluster member nodes. For a single instance deployment,
# `"1 localhost:8100 localhost:8200"` will work just fine.
# Each array value must have the following format:
# `id addr_raft addr_api`
#
# default: ["1 localhost:8100 localhost:8200"]
# overwritten by: HQL_NODES
nodes = [
    "1 localhost:8100 localhost:8200",
    #    "2 localhost:8101 localhost:8201",
    #    "3 localhost:8102 localhost:8202",
]
</code></pre>
<h3 id="secret_raft--secret_api"><a class="header" href="#secret_raft--secret_api"><code>secret_raft</code> + <code>secret_api</code></a></h3>
<p>Since you need both <code>cluster.secret_raft</code> and <code>cluster.secret_api</code> in any case, there is nothing to change here. These
define the secrets being used internally to authenticate against the Raft or the API server for <code>Hiqlite</code>.
You can generate safe values with for instance</p>
<pre><code>cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c48
</code></pre>
<h3 id="tls-1"><a class="header" href="#tls-1">TLS</a></h3>
<p>If you are using a service mesh like for instance <a href="https://linkerd.io/">linkerd</a> which creates mTLS connections between
all pods by default, you can use the HA cache with just plain HTTP, since <code>linkerd</code> will encapsulate the traffic anyway.
In this case, there is nothing to do.</p>
<p>However, if you do not have encryption between pods by default, I would highly recommend, that you use <a href="config/tls.html">TLS</a>.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="bootstrapping"><a class="header" href="#bootstrapping">Bootstrapping</a></h1>
<p>Rauthy supports some basic bootstrapping functionality to make it easier to use in some kind of automated environment.</p>
<h2 id="admin-user"><a class="header" href="#admin-user">Admin User</a></h2>
<p>By default, the admin user will always be <code>admin@localhost</code> and the password will be auto-generated and shown
only once in the logs. This is the easiest way if you want to set up a new instance manually.</p>
<p>However, you can change the default admin username and also provide a bootstrap password for this user, if Rauthy
starts with an empty database for the very first time. This makes it possible to either generate a secure password with
external tooling and have more control over it, or just use the same one all the time, because you may spin up an
instance in test pipelines.</p>
<p>You can change the default admin user E-Mail with:</p>
<pre><code class="language-toml">[bootstrap]
# If set, the email of the default admin will be changed during
# the initialization of an empty production database.
#
# default: 'admin@localhost'
# overwritten by: BOOTSTRAP_ADMIN_EMAIL
admin_email = 'admin@localhost'
</code></pre>
<p>The password can be bootstrapped in 2 ways:</p>
<ul>
<li>Provide a plain test password</li>
<li>Provide the password hashed with argon2id</li>
</ul>
<pre><code class="language-toml">[bootstrap]
# If set, this plain text password will be used for the initial
# admin password instead of generating a random password.
#
# default: random -&gt; see logs on first start
# overwritten by: BOOTSTRAP_ADMIN_PASSWORD_PLAIN
password_plain = '123SuperSafe'

# If set, this will take the Argon2ID hashed password during the
# initialization of an empty production database. If both
# `password_plain` and `pasword_argon2id` are set, the hashed
# version will always be prioritized.
#
# default: random -&gt; see logs on first start
# overwritten by: BOOTSTRAP_ADMIN_PASSWORD_ARGON2ID
pasword_argon2id = '$argon2id$v=19$m=32768,t=3,p=2$mK+3taI5mnA+Gx8OjjKn5Q$XsOmyvt9fr0V7Dghhv3D0aTe/FjF36BfNS5QlxOPep0'
</code></pre>
<h2 id="api-key"><a class="header" href="#api-key">API Key</a></h2>
<p>In automated environments, you would usually not use a normal user to access the Rauthy API, since it is a bit more
cumbersome to use from outside the browser because of additional CSRF tokens and security features. If you want to
automatically set up Rauthy with external tooling after the first startup, you would want to do this with an API key
most probably.</p>
<p>If Rauthy starts up with an empty database, you can bootstrap a single API key with providing a base64 encoded json.</p>
<p>An example json, which would create a key named <code>bootstrap</code> with access to <code>clients, roles, groups</code> with all
<code>read, write, update, delete</code> could look like this:</p>
<pre><code class="language-json">{
  "name": "bootstrap",
  "exp": 1735599600,
  "access": [
    {
      "group": "Clients",
      "access_rights": [
        "read",
        "create",
        "update",
        "delete"
      ]
    },
    {
      "group": "Roles",
      "access_rights": [
        "read",
        "create",
        "update",
        "delete"
      ]
    },
    {
      "group": "Groups",
      "access_rights": [
        "read",
        "create",
        "update",
        "delete"
      ]
    }
  ]
}
</code></pre>
<p>The config documentation for the bootstrap value should explain all further questions:</p>
<pre><code class="language-toml">[bootstrap]
# You can provide an API Key during the initial prod database
# bootstrap. This key must match the format and pass validation.
# You need to provide it as a base64 encoded JSON in the format:
#
# ```
# struct ApiKeyRequest {
#     /// Validation: `^[a-zA-Z0-9_-/]{2,24}$`
#     name: String,
#     /// Unix timestamp in seconds in the future (max year 2099)
#     exp: Option&lt;i64&gt;,
#     access: Vec&lt;ApiKeyAccess&gt;,
# }
#
# struct ApiKeyAccess {
#     group: AccessGroup,
#     access_rights: Vec&lt;AccessRights&gt;,
# }
#
# enum AccessGroup {
#     Blacklist,
#     Clients,
#     Events,
#     Generic,
#     Groups,
#     Roles,
#     Secrets,
#     Sessions,
#     Scopes,
#     UserAttributes,
#     Users,
# }
#
# #[serde(rename_all="lowercase")]
# enum AccessRights {
#     Read,
#     Create,
#     Update,
#     Delete,
# }
# ```
#
# You can use the `api_key_example.json` from `/` as
# an example. Afterward, just
# `base64 api_key_example.json | tr -d '\n'`
#
# overwritten by: BOOTSTRAP_API_KEY
api_key = 'ewogICJuYW1lIjogImJvb3RzdHJhcCIsCiAgImV4cCI6IDE3MzU1OTk2MDAsCiAgImFjY2VzcyI6IFsKICAgIHsKICAgICAgImdyb3VwIjogIkNsaWVudHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6IFsKICAgICAgICAicmVhZCIsCiAgICAgICAgImNyZWF0ZSIsCiAgICAgICAgInVwZGF0ZSIsCiAgICAgICAgImRlbGV0ZSIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImdyb3VwIjogIlJvbGVzIiwKICAgICAgImFjY2Vzc19yaWdodHMiOiBbCiAgICAgICAgInJlYWQiLAogICAgICAgICJjcmVhdGUiLAogICAgICAgICJ1cGRhdGUiLAogICAgICAgICJkZWxldGUiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJncm91cCI6ICJHcm91cHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6IFsKICAgICAgICAicmVhZCIsCiAgICAgICAgImNyZWF0ZSIsCiAgICAgICAgInVwZGF0ZSIsCiAgICAgICAgImRlbGV0ZSIKICAgICAgXQogICAgfQogIF0KfQ=='

</code></pre>
<p>The secret needs to be set with a second variable. Just make sure it contains at least 64 alphanumeric characters.</p>
<pre><code class="language-toml">[bootstrap]
# The secret for the above defined bootstrap API Key.
# This must be at least 64 alphanumeric characters long.
# You will be able to use that key afterward with setting
# the `Authorization` header:
#
# `Authorization: API-Key &lt;your_key_name_from_above&gt;$&lt;this_secret&gt;`
#
# overwritten by: BOOTSTRAP_API_KEY_SECRET
api_key_secret = 'twUA2M7RZ8H3FyJHbti2AcMADPDCxDqUKbvi8FDnm3nYidwQx57Wfv6iaVTQynMh'
</code></pre>
<p>With the values from this example, you will then be able to use the API key with providing the <code>Authorization</code> header
with each request in the following format:</p>
<pre><code>Authorization: API-Key &lt;API Key name&gt;$&lt;API Key secret&gt;
</code></pre>
<pre><code>Authorization: API-Key bootstrap$twUA2M7RZ8H3FyJHbti2AcMADPDCxDqUKbvi8FDnm3nYidwQx57Wfv6iaVTQynMh
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="database-migrations"><a class="header" href="#database-migrations">Database Migrations</a></h1>
<p>You can migrate easily between Hiqlite and Postgres, or just between different instances of the same database.</p>
<p>Let's say you started out by evaluating Rauthy with the default Hiqlite and a single instance deployment. Later on, you
want to migrate to Postgres for whatever reason. Or you started with Postgres and you want to reduce your memory
footprint by switching to Hiqlite. All of this is easily possible.</p>
<p><strong>Solution:</strong> <code>MIGRATE_DB_FROM</code></p>
<p>If you set the <code>MIGRATE_DB_FROM</code> ENV var, it will perform a migration at the next restart.<br />
The way it works is the following:</p>
<ol>
<li>At startup, have a look if <code>MIGRATE_DB_FROM</code> is given</li>
<li>If yes, then connect to the given database</li>
<li>At the same time, connect to the database specified via the normal config values</li>
<li>Overwrite all existing data in the target database with the data from the <code>MIGRATE_DB_FROM</code> source</li>
<li>Close the connection to <code>MIGRATE_DB_FROM</code></li>
<li>Start normal operation</li>
</ol>
<div id="admonition-danger" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-title">
<div class="admonition-title">
<div id="admonition-danger-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="config/db_migration.html#admonition-danger"></a>
</div>
<div>
<p><code>MIGRATE_DB_FROM</code> overwrites any data in the target database! Be very careful with this option.</p>
<p>If you do not remove the <code>MIGRATE_DB_FROM</code> after the migration has been done, it will overwrite the target again with
the next restart of the application. Remove the config variable immediately after the migration has finished.</p>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="unix-domain-sockets"><a class="header" href="#unix-domain-sockets">UNIX Domain Sockets</a></h1>
<p>Rauthy supports listening on a UNIX domain socket. To enable this feature, you will need to specify the following
configurations.</p>
<pre><code class="language-toml">[server]
# The server address to listen on. Can bind to a specific IP.
# When LISTEN_SCHEME is set to use UNIX sockets, this specifies
# the path to listen on.
#
# default: '0.0.0.0'
# overwritten by: LISTEN_ADDRESS
listen_address = "0.0.0.0"

# The scheme to use locally, valid values:
# http | https | http_https | unix_http | unix_https
# For more details about the UNIX domain socket, check out its
# documentation page.
#
# default: http_https
# overwritten by: LISTEN_SCHEME
scheme = "unix_https"

# When rauthy is running behind a reverse proxy, set to true
#
# default: false
# overwritten by: PROXY_MODE
proxy_mode = true
</code></pre>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/unix_socket.html#admonition-caution"></a>
</div>
<div>
<p>Note that <code>unix_https</code> does not make a socket with TLS. When <code>unix_https</code> is used, the socket will still be plain HTTP!</p>
<p><code>unix_https</code> specifies the scheme to be prepended to <code>pub_url</code>, so whether to use<code>unix_http</code> and <code>unix_https</code> depends on the scheme you are using on the reverse-proxy.</p>
</div>
</div>
<p>UNIX domain sockets should be used with a reverse proxy. Without such a proxy, Rauthy could not know the IP address of
visitors, and it will see all visitors as <code>192.0.0.8</code>, the IPv4 dummy address. Rate-limits and IP blacklists will apply
on
all visitors at the same time.</p>
<p>After setting up a reverse proxy, please also remember to set your proxy to send peer IP in headers (for example,
<code>X-Forwarded-For</code>), and set <code>PROXY_MODE</code> to let Rauthy read the IP. Once everything is set, check the <code>/auth/v1/whoami</code>
endpoint to see if your IP address is shown correctly.</p>
<p>It is also recommended to use POSIX ACLs to limit access to Rauthy socket to your reverse proxy, so other UNIX users
won't be able to connect to the socket directly and bypass the reverse proxy.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="tuning"><a class="header" href="#tuning">Tuning</a></h1>
<p>Rauthys default tuning is optimized for somewhat low resource usage while still providing good performance and low
latency up to a couple of thousands of users. If you run a tiny instance with less than 100 users, or a very big one
with users going into millions, or you need a very high degree of concurrency, you can optimize the default tuning.
We will go through a few options to achieve what you need, no matter if you want even lower memory usage or higher
throughput.</p>
<h2 id="database"><a class="header" href="#database">Database</a></h2>
<h3 id="cache_storage_disk"><a class="header" href="#cache_storage_disk"><code>cache_storage_disk</code></a></h3>
<p>By default, Rauthy keeps the WAL + Snapshots for the in-memory cache on disk. This makes the cache, even though it
exists in-memory only, persistent, because it can be rebuilt from disk after a restart. Blacklisted IPs for instance
exists in the cache only, as well as Auth Codes and some other values. The other advantage is, that it reduces the
overall memory usage a bit, because we don't need duplicate data in-memory just to make the Raft replication work.</p>
<p>These advantages are really nice and usually, because the throughput is really high anyway, you should not need to
worry about it. However, you can optionally keep everything cache related in memory only. This boosts the throughput
a lot, depending on the IOPS of your disk of course, at the cost of higher memory usage and the fact, that nodes need
to re-join the Raft cluster after a restart and do a complete re-sync for latest Snapshot + Raft Logs.</p>
<p>To keep everything in memory only, set:</p>
<pre><code class="language-toml">[cluster]
# Set to `false` to store Cache WAL files + Snapshots in-memory only.
# If you run a Cluster, a Node can re-sync cache data after a restart.
# However, if you restart too quickly or shut down the whole cluster,
# all your cached data will be gone.
# In-memory only hugegly increases the throughput though, so it
# depends on your needs, what you should prefer.
#
# default: true
# overwritten by: HQL_CACHE_STORAGE_DISK
cache_storage_disk = false
</code></pre>
<h3 id="read_pool_size--pg_max_conn"><a class="header" href="#read_pool_size--pg_max_conn"><code>read_pool_size</code> / <code>pg_max_conn</code></a></h3>
<p>These values define the database connection pool sizes. <code>pg_max_conn</code> is by default set to <code>20</code>, and
<code>read_pool_size</code> to <code>4</code>.</p>
<p>The Postgres connections are a limit on the maximum and the application will dynamically open as many as necessary. If
you have a huge amount of concurrency, you might see improvements by increasing <code>pg_max_conn</code>. Keep in mind though, that
most data that is used for normal user authentication is cached in memory, so it's possible that it will not help at
all. If you want to lower resource usage as much as possible, you might want to consider a lower value for
<code>pg_max_conn</code>, even though I would only do this for very tiny Rauthy instances with less than 50 users. The connection
pool size for Postgres has a much higher impact than it does for Hiqlite, because of the networking overhead.</p>
<p>For Hiqlite with <code>read_pool_size</code>, you might want to do the same of course. However, the pool is statically built to
reduce latency. On the other hand, a higher amount of connections will only very slightly increase memory usage. These
connections are only for reading data and don't do anything regarding writes. The Hiqlite writer will always be a fixed,
single writer connection running on a dedicated thread.</p>
<p>The Hiqlite read pool can be configured in the <code>cluster</code> section:</p>
<pre><code class="language-toml">[cluster]
# The size of the pooled connections for local database reads.
#
# Do not confuse this with a pool size for network databases, as it
# is much more efficient. You can't really translate between them,
# because it depends on many things, but assuming a factor of 10 is
# a good start. This means, if you needed a (read) pool size of 40
# connections for something like a postgres before, you should start
# at a `read_pool_size` of 4.
#
# Keep in mind that this pool is only used for reads and writes will
# travel through the Raft and have their own dedicated connection.
#
# default: 4
# overwritten by: HQL_READ_POOL_SIZE
#read_pool_size = 4
</code></pre>
<p>The <code>pg_max_conn</code> is located in the <code>database</code> section:</p>
<pre><code class="language-toml">[database]
# Max DB connections for the Postgres pool.
#
# default: 20
# overwritten by: PG_MAX_CONN
pg_max_conn = 20
</code></pre>
<h3 id="log_sync"><a class="header" href="#log_sync"><code>log_sync</code></a></h3>
<p>When using Hiqlite, you can control the Raft Log sync / flush strategy. This has an immediate effect on disk longevity
and crash resistance, but of course also on total throughput.</p>
<pre><code class="language-toml">[cluster]
# Setting for Raft Log syncing / flushing.
#
# This value is probably the most important, depending on your needs.
# It has a huge impact on both performance and consistency.
#
# You can set 3 different levels:
# - immediate
# - immediate_async
# - interval_&lt;time_ms&gt; (e.g. interval_200)
#
# If you run a single instance "Cluster", you most probably want
# `immediate` to have the highest degree of consistency. If set
# to `immediate`, the Raft will block until data has been flushed
# to disk. This is especially important for a single instance
# deployment, because there is no way to recover state from other
# nodes or re-sync a maybe corrupted WAL file.
# `immediate` has a very huge negative impact on throughput, and
# it puts a lot of stress on your disk, which is important to
# consider for SSDs.
#
# `immediate_async` puts the same amount of stress on your SSD
# and flushed all buffers to disk immediately after a single
# Raft Log was saved. Unlike `immediate` however, it does not
# wait for completion and directly returns `success` to the
# Raft Engine. You have a bit less consistency guarantees in
# exchange for basically the same throughput as with `interval`
# syncing.
#
# The `interval_&lt;ms&gt;` option will not flush immediately. Flushes
# will be triggered by an external ticker task top flush buffers
# every &lt;ms&gt; ms, and then only if buffers are dirty, meaning if
# any data has been updated since the last sync. This setting
# has the highest throughput and puts the lowest amount of stress
# on your SSD.
# CAUTION: You probably never want to use this setting for a
# single instance, since it may be the case that if you are
# unlucky and your app crashes before buffers are synced, that
# your WAL file might end up being corrupted. Even though very
# unlikely in real world (can be force-produced though), you
# would need to re-sync from a healthy cluster member in such
# a case, if the automactic WAL repair does not succeed.
#
# default: immediate_async
# overwritten by: HQL_LOG_SYNC
log_sync = "immediate_async"
</code></pre>
<h2 id="wal_size--logs_until_snapshot"><a class="header" href="#wal_size--logs_until_snapshot"><code>wal_size</code> + <code>logs_until_snapshot</code></a></h2>
<p>This applies to Hiqlite only as well. If you only run a small instance, you could save a little bit of memory by
adjusting the WAL file size and the threshold when to create a new Snapshot and cleanup Raft Logs. The default values
will handle as much throughput as your disk IOPS allow. However, you could tune it down for small instance to e.g.</p>
<ul>
<li><code>wal_size = 262144</code></li>
<li><code>logs_until_snapshot = 1000</code></li>
</ul>
<pre><code class="language-toml">[cluster]
# Hiqlite WAL files (when not using the `rocksdb` feature) will
# always have a fixed size, even when they are still "empty", to
# reduce disk operations while writing. You can set the WAL size
# in bytes. The default value is 2 MB, while the minimum size is
# 8 kB.
#
# default: 2097152
# overwritten by: HQL_WAL_SIZE
wal_size = 2097152

# Sets the limit when the Raft will trigger the creation of a new
# state machine snapshot and purge all logs that are included in
# the snapshot.
# Higher values can achieve more throughput in very write heavy
# situations but will end up in more disk usage and longer
# snapshot creations / log purges.
#
# default: 10000
# overwritten by: HQL_LOGS_UNTIL_SNAPSHOT
logs_until_snapshot = 10000
</code></pre>
<p>This will trigger more often WAL log roll-overs, which of course does more syscalls. But since WAL files are
memory-mapped, you can save a little bit of memory at runtime. These values should always be adjusted together. They
both have an impact on how many WAL files will exist during normal operation. ideally, you have 2-4 WAL files around
during normal operation. At the same time, you should make make these values too small so that Snapshots are not being
created more than probably every 15 minutes.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/tuning.html#admonition-note"></a>
</div>
<div>
<p>You should not push the <code>wal_size</code> below <code>131072</code>. The benefits in memory usage will become very small in exchange for
a lot more disk I/O. Also, the <code>wal_size</code> should always be a multiple of <code>4096</code> for optimal alignment. Values bigger
than the default 2MiB will typically not have any positive impact as well, as long as you are not having too many
Snapshots because of a huge amount of concurrent users.</p>
</div>
</div>
<h2 id="http-server"><a class="header" href="#http-server">HTTP Server</a></h2>
<h3 id="http_workers"><a class="header" href="#http_workers"><code>http_workers</code></a></h3>
<p>The <code>server.http_workers</code> variable defines the amount of HTTP worker threads being spawned. This has a big impact on
the amount of memory used. This is especially important when you run Rauthy inside a container on a bigger underlying
host with many cores. Rauthy will spawn more workers depending on the total amount of cores, and not depending on any
container limits you might have set, which it cannot see.</p>
<p>The default value is:</p>
<ul>
<li>less than 4 CPU cores -&gt; 1</li>
<li>4+ cores -&gt; max(2, cores - MAX_HASH_THREADS - reserve), where <code>reserve</code> is 2 when <code>HIQLITE=true</code> and 1 otherwise</li>
</ul>
<p>You almost <strong>always want to tune this</strong> to your needs, it has a big impact on memory! The best idea is probably to use
the same formula, but with respect to custom container limits, or whatever you would want Rauthy to use.</p>
<pre><code class="language-toml">[server]
# Limits the amount of HTTP worker threads. This value
# heavily impacts memory usage, even in idle. The default
# values are:
# - less than 4 CPU cores -&gt; 1
# - 4+ cores -&gt; max(2, cores - MAX_HASH_THREADS - reserve)
#   where `reserve` is 2 when `HIQLITE=true` and 1 otherwise.
#
# CAUTION: If you run your instance on a big underlying host,
# you almost always want to manually set an appropriate
# value. Rauthy can only see all available cores and not any
# possibly set container limits. This means if it runs inside
# a container on something like a 96 core host, Rauthy will
# by default spawn very many threads.
#
# overwritten by: HTTP_WORKERS
http_workers = 1
</code></pre>
<h3 id="metrics_enable"><a class="header" href="#metrics_enable"><code>metrics_enable</code></a></h3>
<p>In newer versions, the <code>metrics_enable</code> is <code>false</code> by default and opt-in. You can enable prometheus metrics with it. An
independent HTTP server will be spawned, it will consume additional memory and CPU. Leave it to <code>false</code> if not needed.</p>
<pre><code class="language-toml">[server]
# To enable or disable the additional HTTP server to expose
# the /metrics endpoint.
#
# default: false
# overwritten by: METRICS_ENABLE
metrics_enable = true
</code></pre>
<h3 id="swagger_ui_enable"><a class="header" href="#swagger_ui_enable"><code>swagger_ui_enable</code></a></h3>
<p>If enable <code>swagger_ui_enable</code> and set it to <code>true</code>, it will consume ~13 MB of additional memory. To reduce overall
memory fragmentation further down the road, if enabled, it will be initialized at the very start of the application
instead of being lazily initialized. If you don't need the API documentation, leave it to the default, which is <code>false</code>.</p>
<pre><code class="language-toml">[server]
# Can be set to `true` to enable the Swagger UI.
# This will consume ~13mb of additional memory.
#
# default: false
# overwritten by: SWAGGER_UI_ENABLE
swagger_ui_enable = true
</code></pre>
<h2 id="password-hashing"><a class="header" href="#password-hashing">Password Hashing</a></h2>
<h3 id="argon2_m_cost--argon2_t_cost--argon2_p_cost--max_hash_threads"><a class="header" href="#argon2_m_cost--argon2_t_cost--argon2_p_cost--max_hash_threads"><code>argon2_m_cost</code> + <code>argon2_t_cost</code> + <code>argon2_p_cost</code> + <code>max_hash_threads</code></a></h3>
<p>Values for password hashing are probably the most important ones to tune overall. They do not only dictate resource
usage, but also the strength and security of password hashes. The password hashing will always be the limiting factor,
at least as long as not all of your users use Webauthn-only accounts. This book, as well as the Admin UI, have dedicated
sections and utilities for these values. Just keep in mind to tune them properly.</p>
<pre><code class="language-toml">[hashing]
# Argon2ID hashing parameters. Take a look at the documentation
# for more information:
# https://sebadob.github.io/rauthy/config/argon2.html
# M_COST must never be below 32768 in production
# overwritten by: ARGON2_M_COST
argon2_m_cost = 131072
# T_COST must never be below 1 in production
# overwritten by: ARGON2_T_COST
argon2_t_cost = 4
# P_COST must never be below 2 in production
# overwritten by: ARGON2_P_COST
argon2_p_cost = 8

# Limits the maximum amount of parallel password hashes at the exact same time
# to never exceed system memory while still allowing a good amount of memory
# for the Argon2ID algorithm
#
# CAUTION: You must make sure, that you have at least
# (MAX_HASH_THREADS * ARGON2_M_COST / 1024) + idle memory of your deployment available.
#
# default: 2
# overwritten by: MAX_HASH_THREADS
max_hash_threads = 2
</code></pre>
<h2 id="memory-allocator"><a class="header" href="#memory-allocator">Memory Allocator</a></h2>
<p>Rauthy uses <code>jemalloc</code> under the hood, which you can tune via an environment variable, as long as it's set before the
application starts. A default setting is baked into the compiled binary, but when you provide the env var, you can
overwrite it. The default is tuned to be just fine for instances with a couple of hundreds of users and focuses more
on being efficient rather than providing a high degree of concurrency.</p>
<p>If you have a bigger instance with many thousands or even millions of users, or just a tiny one with probably less than
100, you can optimize it. There is only a small amount of room in terms of lowering memory usage, but a lot of
increasing concurrency at the cost of higher memory usage.</p>
<p>I will not go into the details of <code>jemalloc</code> here. If you are interested in this topic, you will find lots of
information via the search engine of your choice. I only want to give you recipes that should just work.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/tuning.html#admonition-caution"></a>
</div>
<div>
<p>You need to set the <code>MALLOC_CONF</code> environment variable from the recipes below BEFORE the application starts, or
otherwise it will be ignored. Anything like the <code>-e</code> flag for docker, or a regular environment variable inside K8s will
work.</p>
</div>
</div>
<h3 id="small-instance"><a class="header" href="#small-instance">Small Instance</a></h3>
<p>If you run a small instance with less than 100 users and you want to reduce the memory footprint as much as possible,
you have two options.</p>
<p><strong>1. Lowest memory usage</strong></p>
<p>This value will probably give you no noticeable performance hit while still providing a low memory usage.</p>
<pre><code>MALLOC_CONF=abort_conf:true,narenas:1,tcache_max:1024,dirty_decay_ms:1000,muzzy_decay_ms:1000
</code></pre>
<p><strong>2. Lowest possible memory at all costs.</strong></p>
<p>This will come with a <strong>very big performance hit</strong>! You should only use it, if you don't care about performance at all.
Imho, the performance hit is not worth the small savings compared to the config above.</p>
<pre><code>MALLOC_CONF=abort_conf:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0
</code></pre>
<h3 id="medium-instance"><a class="header" href="#medium-instance">Medium Instance</a></h3>
<p>If you run a medium instance with up to 500-1000 users, you are probably just fine with the defaults. Stick to them.
Just for reference, the currently used default config is this:</p>
<pre><code>MALLOC_CONF=abort_conf:true,narenas:8,tcache_max:4096,dirty_decay_ms:5000,muzzy_decay_ms:5000
</code></pre>
<h3 id="big-instance"><a class="header" href="#big-instance">Big Instance</a></h3>
<p>A big instance is one with more than at least 1000 users. In this case, you might not get the desired performance,
especially if you have a higher value for <code>MAX_HASH_THREADS</code> or <code>HTTP_WORKERS</code>. Most requests are so small, that they
almost entirely fit in the thread-local cache for the allocator, but if dynamic brotli compression kicks in, memory
from an allocator arena will be necessary most of the time. Because Rauthy does not need the arena's that often, the
default value of <code>4</code> is too low in such a case. You should set <code>narenas</code> from the string below at least equal to the
amount of CPU cores your Rauthy instance is assigned to. Depending on the amount or concurrent logins, you may set
<code>narenas</code> to 2-4x CPU cores at the cost of higher memory usage.</p>
<pre><code>MALLOC_CONF=abort_conf:true,narenas:16,tcache_max:16384,dirty_decay_ms:10000,muzzy_decay_ms:10000
</code></pre>
<h3 id="open-end"><a class="header" href="#open-end">Open End</a></h3>
<p>For any instance with basically an open end of users, or if you have a very high degree of concurrent logins (or you
just don't care about low memory usage and only about max performance), set <code>narenas</code> to 4x your CPU cores (higher
values will start to have a negative impact). This config will let you scale into millions of users easily.</p>
<pre><code>MALLOC_CONF=abort_conf:true,narenas:64,tcache_max:32768,dirty_decay_ms:30000,muzzy_decay_ms:30000
</code></pre>
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
<div class="admonition-title">
<div id="admonition-note-1-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="config/tuning.html#admonition-note-1"></a>
</div>
<div>
<p>The Memory Allocator tuning does not work on Windows msvc targets, if you are running a custom-compiled version of
Rauthy. It should work everywhere else though.</p>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="upstream-authentication-providers"><a class="header" href="#upstream-authentication-providers">Upstream Authentication Providers</a></h1>
<p>This section is about upstream authentication providers, for instance things like <strong>Sign in with Github</strong>.</p>
<p>This feature can be considered "in beta state" and the only setup guide is for Github for now.<br />
Additional ones may come, when people are using other providers and give feedback or actually contribute to these docs.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="github-provider"><a class="header" href="#github-provider">Github Provider</a></h1>
<p>To set up Github as an upstream provider for Rauthy, you have pretty little things to do.<br />
Rauthy includes a Template for Github already, which makes this whole process pretty simple.</p>
<h2 id="create-an-oauth-app"><a class="header" href="#create-an-oauth-app">Create an OAuth App</a></h2>
<ol>
<li>Log in to your Github account</li>
<li>Navigate to
<a href="https://github.com/settings/applications/new">Settings -&gt; Developer Settings -&gt; OAuth Apps -&gt; Register a new application</a></li>
<li>Provide any name for your application. This is not important for the flow, only for your users to recognize</li>
<li>Homepage URL should be the URL of your Rauthy deployment</li>
<li><code>Authorization callback URL</code> is the only really important one here. For instance, if your Rauthy instance would live
at <code>https://iam.example.com</code>, your callback URL would be <code>https://iam.example.com/auth/v1/providers/callback</code></li>
<li>Don't enable Device Flow and <code>Register application</code></li>
</ol>
<p><img src="auth_providers/img/1_github_app_create.png" alt="1_github_app_create" /></p>
<p>On the following page, you can upload a logo and so on, but the important information is:</p>
<ul>
<li><code>Client ID</code></li>
<li><code>Generate a new client secret</code></li>
</ul>
<p>Both of these values need to be inserted into Rauthy's Admin UI in the next step, so lets open this in a new tab.</p>
<h2 id="rauthy-provider-config"><a class="header" href="#rauthy-provider-config">Rauthy Provider Config</a></h2>
<ol>
<li>Log in to your Rauthy Admin UI, navigate to <code>Providers</code> and add a new provier.</li>
<li>Choose <code>Github</code> as the <code>Type</code> and leave the Custom Root CA and insecure TLS unchecked.</li>
</ol>
<p><img src="auth_providers/img/2_rauthy_add_new.png" alt="1_github_app_create" /></p>
<ol start="3">
<li>The template pre-populates most of the inputs already for you. The only information you need to add here is
<ul>
<li><code>Client ID</code> -&gt; copy <code>Client ID</code> from the Github OAuth App page inside here</li>
<li><code>Client Secret</code> -&gt; on Github, <code>Generate a new client secret</code> and copy &amp; paste the value</li>
<li>you may edit the <code>Client Name</code> if you want something else than <code>Github</code></li>
</ul>
</li>
</ol>
<p><img src="auth_providers/img/3_rauthy_provider_config.png" alt="1_github_app_create" /></p>
<ol start="5">
<li>Leave the rest untouched and hit <code>Save</code></li>
<li>You should expand the saved Github provider in the Rauthy UI again after saving and upload a Logo for the Client.
Currently, Rauthy does not provide default Logos, though these might be added at some point. Just <code>UPLOAD LOGO</code> from
your local disk.</li>
</ol>
<h2 id="finish"><a class="header" href="#finish">Finish</a></h2>
<p>That's it. Now log out and try to log in to your Account view. You should see your new upstream provider on the Login
page.</p>
<p><img src="auth_providers/img/4_rauthy_github_login.png" alt="1_github_app_create" /></p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="working-with-rauthy"><a class="header" href="#working-with-rauthy">Working with Rauthy</a></h1>
<p>This section will describe a few things you should now when working with Rauthy</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="api-keys"><a class="header" href="#api-keys">API Keys</a></h1>
<p>API Keys allow you to interact with Rauthy's REST API in a programmatic way. They allow you to do almost all the same
things as a logged in Admin via the UI is allowed to do. There are only very few exceptions, like for instance an API
Key is not allowed to manage other API Keys to reduce the impact of leaked credentials somewhere in your applications.</p>
<p>Each API Key can be configured with very fine-grained access rights, so you only allow actions it actually needs to
perform for improved security.</p>
<p>Setting these access rights should be very intuitive. For instance, if you want to use your API Key to listen to the
events stream, you need to allow <em>Events: read</em>, which makes sense. If you want to read users and maybe update them in
some external application, you would need <em>Users: read, update</em>.<br />
Most access rights requests mimic their HTTP methods:</p>
<ul>
<li><code>GET</code> -&gt; <code>read</code></li>
<li><code>POST</code> -&gt; <code>create</code></li>
<li><code>PUT</code> -&gt; <code>update</code></li>
<li><code>DELETE</code> -&gt; <code>delete</code></li>
</ul>
<p>To use an API Key, you need to provide is with the HTTP <code>Authorization</code> header, but instead of <code>Bearer</code> as prefix, you
set it to <code>API-Key</code>, so Rauthy can efficiently distinguish if you try to access the API with a JWT token or an API Key:</p>
<pre><code>Authorization: API-Key &lt;API Key name&gt;$&lt;API Key secret&gt;
</code></pre>
<pre><code>Authorization: API-Key my_key$twUA2M7RZ8H3FyJHbti2AcMADPDCxDqUKbvi8FDnm3nYidwQx57Wfv6iaVTQynMh
</code></pre>
<p>The correct format will be generated automatically, when you create a new secret for an API Key via the Admin UI, so
you only need to copy &amp; paste it.</p>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/api_keys.html#admonition-info"></a>
</div>
<div>
<p>The only actions API Keys are not allowed to do is interacting with other API Keys and external SSO Providers for
logins to limit the likelyhood of priviledge escalation in case of leaked credentials.</p>
</div>
</div>
<h2 id="creating-a-new-api-key"><a class="header" href="#creating-a-new-api-key">Creating a new API Key</a></h2>
<p>Apart from the initial <a href="work/../config/bootstrap.html#api-key">bootstrap</a>, the creation and modification of API Keys is only
allowed via the Admin UI.</p>
<p>Navigate to the <code>API Keys</code> section, click <code>New Key</code>, fill out the inputs and <code>Save</code>. You can set an optional key expiry.
By default, API Keys never expire.</p>
<p><img src="work/../config/img/api_key_1.png" alt="api key create" /></p>
<p>The secret is created in a second step. Whenever you generate a new secret, the old one will stop working immediately.</p>
<p><img src="work/../config/img/api_key_2.png" alt="api key secret" /></p>
<p>After generating a new secret, you will never be able to see it again. If you loose it, you must generate a new one.
Rauthy will format the API key in the correct format and shows a <code>curl</code> command for making sure your key works fine.
You can copy &amp; paste the pre-configured <code>curl</code> for the test endpoint. The result should look something like this:</p>
<p><img src="work/../config/img/api_key_test.png" alt="api key test" /></p>
<p>If you try to access an endpoint with an API Key that has insufficient access rights, Rauthy will return a proper
error message with description, which access rights you actually need.</p>
<p><img src="work/../config/img/api_key_permission.png" alt="api key permission" /></p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="custom-scopes-and-attributes"><a class="header" href="#custom-scopes-and-attributes">Custom Scopes and Attributes</a></h1>
<p>OpenID Connect defines quite a few very useful claims to be included in the <code>id_token</code> as additional information about
a user. However, there are times when you need other data as well, and maybe it is data that multiple downstream
applications need, like for instance an internal employee ID.</p>
<p>Rauthy can handle this as well. There are custom scopes you can create. These do not get mixed up with the default
scopes, so they can't interfere, which makes the whole setup pretty easy.</p>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/custom_scopes_attributes.html#admonition-info"></a>
</div>
<div>
<p>Users are not allowed to self-manage their custom scope values.<br />
This must be done by an admin.</p>
</div>
</div>
<p>Values are mapped as claims into tokens only if allowed an requested for an individual client. This means even if you
set a value for a user, it will not just end up in the token automatically. Let's walk through how you can achieve that.</p>
<h2 id="custom-user-attribute"><a class="header" href="#custom-user-attribute">Custom User Attribute</a></h2>
<p>The first thing we will do is to create a custom user attribute. Rauthy does not allow you to just enter anything you
like. All values are strictly typed and must be pre-configured to prevent user error during normal operation. For
instance, you won't be able to do typos, don't need copy &amp; paste from other entries, and so on.</p>
<p>In the Admin UI, navigate to <code>Attributes</code> -&gt; <code>Add New User Attribute</code></p>
<p><img src="work/img/cust_attr.png" alt="custom attribute" /></p>
<p>The description is optional and can be changed at any time without any impact. The <code>name</code> of the attribute will be the
exact same in the JWT token later on.</p>
<h2 id="set-user-values"><a class="header" href="#set-user-values">Set User Values</a></h2>
<p>Now that we created our custom attribute in the step above, we can set them for users. Navigate to a user of your
choice, expand the entry and choose the <code>Attributed</code> tab.</p>
<p><img src="work/img/set_user_attr.png" alt="set user attribute" /></p>
<div id="admonition-info-1" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-1-title">
<div class="admonition-title">
<div id="admonition-info-1-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/custom_scopes_attributes.html#admonition-info-1"></a>
</div>
<div>
<p>The custom values for users are always interpreted, saved and returned <code>String</code>s without any further validation.</p>
</div>
</div>
<h2 id="create-a-custom-scope"><a class="header" href="#create-a-custom-scope">Create a Custom Scope</a></h2>
<p>The next thing we need is a custom scope we can map attributes to. You are not allowed to modify the OIDC default
scopes, but you can add your own ones, as many as you like. This is the first step we need to do. Navigate to the</p>
<p>Admin UI -&gt; <code>Scopes</code> -&gt; <code>Add New Scopes</code></p>
<p><img src="work/img/cust_scope.png" alt="custom scope" /></p>
<p>After saving, expand your just created scope. You want to map your attribute from the step before into JWT tokens
when a client requests (and is allowed to) this scope. For instance, let's add our new attribute to the <code>id_token</code>
with this custom scope.</p>
<p><img src="work/img/cust_scope_mapping.png" alt="custom scope mapping" /></p>
<p>We are almost done.</p>
<h2 id="allow-scope-for-client"><a class="header" href="#allow-scope-for-client">Allow Scope for Client</a></h2>
<p>The very last step is one you probably know. A client needs to be allowed to request certain scopes in the first place.
From this point on, the new custom scope behaves like any default one. We need to allow it for a client to be requested.</p>
<p>Open the configuration for any <code>Client</code> and scroll down until you see the <code>Allowed Scopes</code> and <code>Default Scopes</code> section.</p>
<p><img src="work/img/client_scope_allow.png" alt="allow client scope request" /></p>
<p>The <code>Allowed Scopes</code> are the ones a client is allowed to request when redirecting a user to the authorization endpoint
to initiate the <code>authorization_code</code> flow. The <code>Default Scopes</code> are the ones that Rauthy will simply always add. This is
useful for instance when you are using <code>client_credentials</code> or another flow, because only the <code>authorization_code</code>
flow can request specific scopes while all others can't.</p>
<h2 id="finish-1"><a class="header" href="#finish-1">Finish</a></h2>
<p>That is it. It seems like many steps to follow, but it is to your advantage. With this setup you have type-safety and
the most flexibility about which client can see which values in which context, and so on.</p>
<p>When you have done everything right, you will see your new custom value in the <code>custom</code> section of the JWT token claims.
I used the <code>rauthy</code> client in this example (which you should not mess with when deployed).</p>
<pre><code class="language-json">{
  "iat": 1721722389,
  "exp": 1721722399,
  "nbf": 1721722389,
  "iss": "http://localhost:8080/auth/v1",
  "sub": "za9UxpH7XVxqrtpEbThoqvn2",
  "aud": "rauthy",
  "nonce": "Fars0wPqrm9f6XimDKD08CPl",
  "azp": "rauthy",
  "typ": "Id",
  "amr": [
    "pwd"
  ],
  "auth_time": 1721722389,
  "at_hash": "Hd5ugcSzxwl1epF7Il3pEpq0gznnqs2SnSVZCdNw0EI",
  "preferred_username": "admin@localhost",
  "roles": [
    "rauthy_admin",
    "admin"
  ],
  "custom": {
    "my_attr": "This is Batman!"
  }
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ephemeral-clients"><a class="header" href="#ephemeral-clients">Ephemeral Clients</a></h1>
<p>In addition to static, upfront registered clients, which should always be preferred if they work out for your, and
dynamically registered clients via OIDC DCR, Rauthy supports an additional feature, which is <em>Ephemeral Clients</em>.
Originally implemented when the support for SolidOIDC came, this feature is fully working on its own. These are very
easy to use and provide a completely new functionality, but keep in mind that these are the least efficient of the 3,
as they require an additional network round trip to where ever their document is hosted (apart from caching).</p>
<p>Just to make clear what I am talking about, a high level comparison of these different clients:</p>
<p><strong>Static clients</strong> are the ones that you register and configure via the Admin UI. These are the most efficient and
secure ones. They require less work in the backend and exist inside the Rauthy database.</p>
<p>Then there is OIDC <strong>Dynamic Client Registration</strong> (DCR), which is an OIDC extension that Rauthy supports as well. If a
downstream application has support for this feature, it can self-register a client, which then can be used for the
login afterward. This sound very nice in the beginning, but brings quite a few problems:</p>
<ul>
<li>If the endpoint is open, anyone can register a client, also bots and spammers</li>
<li>If the endpoint is secured an additional token for the registration, there is not much benefit from it, because the
token must be communicated upfront anyway. If so, you could simply register a faster and more efficient static client.</li>
<li>The downstream application must manage its own client. If they don't do it properly, they can spam Rauthys database
again.</li>
<li>As mentioned already, dynamic clients are a bit less efficient than static ones and require at least one additional
database round trip during each login.</li>
</ul>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-info"></a>
</div>
<div>
<p>In case of an open, unauthenticated endpoint for DCR, Rauthy has a simple internal cleanup logic. For instance, if a
dynamic client is registered by bots or spammers and is not used within the first hour after the registration, it will
be deleted automatically. Usually, when an app registers via DCR, the client will be used immeditately for at least one
login.</p>
</div>
</div>
<p>As the third option, Rauthy gives you the ability to use <strong>Ephemeral Clients</strong>. These are disabled by default, because
they are not a default OIDC feature yet (There is an open Draft to include them though). Ephemeral Clients provide
way fewer configuration options, but make it possible that user can self-manage their own clients without any additional
upfront tasks on Rauthy's side. This means as long as you have an account on a Rauthy instance with this feature
enabled, you would not even need an admin to be able to connect any new application to this Rauthy instance.</p>
<p>The client definition exists in some JSON document that is hosted on some domain the user controls, which must be
reachable by Rauthy. The format must be parsable by Rauthy of course, which I will mention later. Whatever information
you provide in this JSON document, Rauthy will use it to authenticate your login request with that very client. The
most important part about this is, that the <code>client_id</code> <strong>must match</strong> the exact URI the document is hosted on.<br />
For instance, let's say you have a JSON on a Github repo at
<a href="https://github.com/sebadob/rauthy/blob/main/ephemeral_client_example.json">https://github.com/sebadob/rauthy/blob/main/ephemeral_client_example.json</a>
, then the <code>client_id</code> <strong>must</strong> be</p>
<pre><code>"client_id": "https://github.com/sebadob/rauthy/blob/main/ephemeral_client_example.json"
</code></pre>
<div id="admonition-danger" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-title">
<div class="admonition-title">
<div id="admonition-danger-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-danger"></a>
</div>
<div>
<p>It is very important that the domain for an ephemeral client JSON is under your control and no one else you don't fully
trust has access. Otherwise, someone else could modify this JSON which affects your login flow. For instance, add an
additional allowed <code>redirect_uri</code> and trick you into a scam website.</p>
</div>
</div>
<p>To keep the whole setup as simple as possible, apart from the <code>client_id</code>, there is only one other mandatory field
in this document, which is <code>redirect_uris</code>. This must be an array with at least one allowed redirect URI which is
allowed during the login flow. Rauthy allows a wildcard <code>*</code> at the end of given redirect uri, but be careful with these.
Wildcards should only be used during initial setup and testing, when you don't know the final URI yet.</p>
<div id="admonition-danger-1" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-1-title">
<div class="admonition-title">
<div id="admonition-danger-1-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-danger-1"></a>
</div>
<div>
<p>Technically, it is possible to just use a single <code>*</code> as a <code>redirect_uri</code>. This would allow any
redirect URI without further validation. You must never use this apart from maybe some internal DEV or testing setups!</p>
</div>
</div>
<p>The JSON document for the ephemeral clients follows the same rules and works in the same ways any other OIDC client.</p>
<h2 id="configuration-1"><a class="header" href="#configuration-1">Configuration</a></h2>
<p>The support for ephemeral clients is opt-in. You need to set at least</p>
<pre><code class="language-toml">[ephemeral_clients]
# Can be set to 'true' to allow the dynamic client lookup via
# URLs as 'client_id's during authorization_code flow initiation.
#
# default: false
# overwritten by: ENABLE_EPHEMERAL_CLIENTS
enable = false
</code></pre>
<p>Apart from this, there are more options you can modify. Ephemeral clients are the least flexible, and they share some
common restrictions. These are valid for all ephemeral clients used with this instance, no matter if they define
something else in their JSON document. This is important so a Rauthy admin can configure a security standard that
can't be broken or ignored.</p>
<pre><code class="language-toml">[ephemeral_clients]
# If set to 'true', MFA / Passkeys will be forced for ephemeral
# clients.
#
# default: false
# overwritten by: EPHEMERAL_CLIENTS_FORCE_MFA
force_mfa = false

# The allowed flows for ephemeral clients.
#
# default: ['authorization_code', 'refresh_token']
# overwritten by: EPHEMERAL_CLIENTS_ALLOWED_FLOWS - single String, \n separated values
allowed_flows = ['authorization_code', 'refresh_token']

# The allowed scopes separated by ' ' for ephemeral clients.
#
# default: ['openid', 'profile', 'email', 'webid']
# overwritten by: EPHEMERAL_CLIENTS_ALLOWED_SCOPES - single String, \n separated values
allowed_scopes = ['openid', 'profile', 'email', 'webid']
</code></pre>
<p>If you need support for Solid OIDC, you need to at least enable web IDs and the solid <code>aud</code>:</p>
<pre><code class="language-toml">[ephemeral_clients]
# Can be set to 'true' to enable WebID functionality like needed
# for things like Solid OIDC.
#
# default: false
# overwritten by: ENABLE_WEB_ID
enable_web_id = false

# If set to 'true', 'solid' will be added to the 'aud' claim from
# the ID token for ephemeral clients.
#
# default: false
# overwritten by: ENABLE_SOLID_AUD
enable_solid_aud = false
</code></pre>
<p>The last option is the caching timeout. The cache for these documents is very important for performance. Without any
cache, Rauthy would need to do an additional network round trip to the client JSON <strong>with each login</strong>. This is very
inefficient, because usually these documents rarely change after the initial setup.</p>
<pre><code class="language-toml">[ephemeral_clients]
# The lifetime in seconds ephemeral clients will be kept inside
# the cache.
#
# default: 3600
# overwritten by: EPHEMERAL_CLIENTS_CACHE_LIFETIME
cache_lifetime = 3600
</code></pre>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-hint"></a>
</div>
<div>
<p>If you are developing or testing, set the <code>cache_lifetime</code> to a very low value. Otherwise, Rauthy would not
see any changes you do to this document as long as the internal cache has not expired.</p>
</div>
</div>
<h2 id="minimal-client-json-document"><a class="header" href="#minimal-client-json-document">Minimal Client JSON Document</a></h2>
<p>The minimal requirements for the client JSON were described above already. They are <code>client_id</code> and <code>redirect_uris</code>.
This means a minimal example JSON could look like this</p>
<pre><code class="language-json">{
  "client_id": "https://example.com/my_ephemeral_client.json",
  "redirect_uris": [
    "https://this.is.where/my/website/is"
  ]
}
</code></pre>
<p>If this is all you care about, you're done.<br />
If you want to configure some more things, keep reading.</p>
<h2 id="advanced-client-json-document"><a class="header" href="#advanced-client-json-document">Advanced Client JSON Document</a></h2>
<p>All values in the client document are validated of course. Below you see a simplified version of the current Rust
<code>struct</code> Rauthy tries to deserialize the document into:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct EphemeralClientRequest {
    /// Validation: `^[a-zA-Z0-9,.:/_\\-&amp;?=~#!$'()*+%]{2,256}$`
    client_id: String,
    /// Validation: `[a-zA-Z0-9À-ÿ-\\s]{2,128}`
    client_name: Option&lt;String&gt;,
    /// Validation: `[a-zA-Z0-9,.:/_-&amp;?=~#!$'()*+%]+$`
    client_uri: Option&lt;String&gt;,
    /// Validation: `Vec&lt;^[a-zA-Z0-9\+.@/]{0,48}$&gt;`
    contacts: Option&lt;Vec&lt;String&gt;&gt;,
    /// Validation: `Vec&lt;^[a-zA-Z0-9,.:/_\\-&amp;?=~#!$'()*+%]+$&gt;`
    redirect_uris: Vec&lt;String&gt;,
    /// Validation: `Vec&lt;^[a-zA-Z0-9,.:/_\\-&amp;?=~#!$'()*+%]+$&gt;`
    post_logout_redirect_uris: Option&lt;Vec&lt;String&gt;&gt;,
    /// Validation: `Vec&lt;^(authorization_code|client_credentials|urn:ietf:params:oauth:grant-type:device_code|password|refresh_token)$&gt;`
    grant_types: Option&lt;Vec&lt;String&gt;&gt;,
    /// Validation: `60 &lt;= access_token_lifetime &lt;= 86400`
    default_max_age: Option&lt;i32&gt;,
    /// Validation: `[a-z0-9-_/:\s*]{0,512}`
    scope: Option&lt;String&gt;,
    require_auth_time: Option&lt;bool&gt;,
    /// Validation: `^(RS256|RS384|RS512|EdDSA)$`
    access_token_signed_response_alg: Option&lt;JwkKeyPairAlg&gt;,
    /// Validation: `^(RS256|RS384|RS512|EdDSA)$`
    id_token_signed_response_alg: Option&lt;JwkKeyPairAlg&gt;,
}
<span class="boring">}</span></code></pre></pre>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-note"></a>
</div>
<div>
<p>The <code>grant_types</code> exist for completeness only. They are actually ignored and overridden by the
<code>EPHEMERAL_CLIENTS_ALLOWED_FLOWS</code> config variable.</p>
</div>
</div>
<p>With this information, we can now build a complete example JSON with all the possible values you can set:</p>
<pre><code class="language-json">{
  "client_id": "https://example.com/my_ephemeral_client.json",
  "client_name": "My Ephemeral Client",
  "client_uri": "https://this.is.where",
  "contacts": [
    "mail@ephemeral.info"
  ],
  "redirect_uris": [
    "https://this.is.where/my/website/is"
  ],
  "post_logout_redirect_uris": [
    "https://this.is.where"
  ],
  "default_max_age": 300,
  "scope": "openid email custom",
  "require_auth_time": true,
  "access_token_signed_response_alg": "RS256",
  "id_token_signed_response_alg": "RS256"
}
</code></pre>
<div id="admonition-hint-1" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-1-title">
<div class="admonition-title">
<div id="admonition-hint-1-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="work/ephemeral_clients.html#admonition-hint-1"></a>
</div>
<div>
<p>Rauthy fetches the document with an <code>Accept: application/json</code> header. This makes it possible to host your
file on Github for instance and receive a valid JSON.</p>
</div>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="e-mail-templates"><a class="header" href="#e-mail-templates">E-Mail Templates</a></h1>
<p>The templates for E-Mails being sent by Rauthy are baked into the binary. This is the most efficient and fastest way
of handling them. It simplifies the container images and speeds up the internal generation of new ones from templates
compared to dynamic ones. At the same time, Rauthy checks these templates for correctness at compile time. It makes
sure, that each templated value exists in the internal application logic and vice versa.</p>
<p>If you need to modify the content of these E-Mails, you have 2 options:</p>
<ul>
<li>Use the pre-defined config variables</li>
<li>Clone the repo, modify the templates and build it from source</li>
</ul>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/email_templates.html#admonition-info"></a>
</div>
<div>
<p>You can only modify the <em>New Password</em> and <em>Password Reset</em> E-Mail currently. There was no need yet for any of the
others.</p>
</div>
</div>
<h2 id="existing-config-values"><a class="header" href="#existing-config-values">Existing Config Values</a></h2>
<p>Modifying the E-Mail contents via config variables is very straight forward. You don't need to compile from source,
therefore can't make any mistakes and for all variables that are not set, Rauthy will simply fall back to the default.</p>
<p>You can take a look at the raw templates directly in the repo, so you can see which value will be mapped to which
location in the final message.</p>
<p><a href="https://github.com/sebadob/rauthy/blob/main/templates/email/reset.txt">Text Version</a></p>
<p><a href="https://github.com/sebadob/rauthy/blob/main/templates/email/reset.html">HTML Version</a></p>
<p>The values are given as multiplem <code>[[templates]]</code> blocks. Each given block must match a separate <code>lang</code> / <code>typ</code>
combination to be valid:</p>
<pre><code class="language-toml">[[templates]]
# You can overwrite some default email templating values.
# If you want to modify the basic templates themselves, this is
# only possible with a custom build from source. The content
# however can mostly be set here.
# If the below values are not set, the default will be taken.
#
# NOTE: This is an array value, and you can specify it multiple
# times for different `lang` / `typ` combinations.

# one of: en de ko zh_hans
lang = 'en'
# pme of: password_new, password_reset
typ = 'password_new'

subject = 'New Password'
header = 'New password for'
text = ''
click_link = 'Click the link below to get forwarded to the password form.'
validity = 'This link is only valid for a short period of time for security reasons.'
expires = 'Link expires:'
footer = ''
</code></pre>
<h2 id="modify-templates-directly"><a class="header" href="#modify-templates-directly">Modify Templates Directly</a></h2>
<p>Modifying templates directly gives you the most amount of control of course. You can change everything you like about
them. However, you need to clone the whole repo, modify the templates in
<a href="https://github.com/sebadob/rauthy/tree/main/templates/email">templates/email/</a>, and then rebuild the whole project
from source on your own. The existing setup should make it pretty easy to do. To get started, take a look at the
<a href="https://github.com/sebadob/rauthy/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a>.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ip-blacklisting"><a class="header" href="#ip-blacklisting">IP Blacklisting</a></h1>
<p>Rauthy can blacklist certain IP that should be prevented from even trying to access it.<br />
Each blacklisting will always have an expiry. This is important because most client IPs will be ephemeral.</p>
<h2 id="automatic-blacklisting"><a class="header" href="#automatic-blacklisting">Automatic Blacklisting</a></h2>
<p>Blacklisting with different timeouts will happen automatically, for instance when thresholds for invalid logins have
been reached or it is obvious that someone is scanning Rauthys API. The scan detection is very basic, but will catch
suspicious requests and bots automatically.</p>
<p>With failed logins, not only the timeout will be increased to send an answer back to the client to prevent brute-force
attacks. Rauthy also has the following default thresholds for blacklisting IPs:</p>
<div class="table-wrapper"><table><thead><tr><th>failed logins</th><th>blacklist</th></tr></thead><tbody>
<tr><td>7</td><td>1 minute</td></tr>
<tr><td>10</td><td>10 minutes</td></tr>
<tr><td>15</td><td>15 minutes</td></tr>
<tr><td>20</td><td>1 hour</td></tr>
<tr><td>25</td><td>1 day</td></tr>
<tr><td>25+</td><td>1 day</td></tr>
</tbody></table>
</div>
<p>In addition to blacklisting, the timeout's for failed logins in between these steps will be longer the higher the
failed attempts counter is.</p>
<h2 id="suspicious-request-blacklisting"><a class="header" href="#suspicious-request-blacklisting">Suspicious Request Blacklisting</a></h2>
<p>As mentioned already, Rauthy has basic capabilities to detect API scanners and bots. These are called <em>suspicious
requests</em> internally.</p>
<p>By default, Rauthy will immediately block such IPs if it detects them. However, you may not want this for whatever
reason. You can modify this behavior with the following config setting:</p>
<pre><code class="language-toml">[suspicious_requests]
# The "catch all" route handler on `/` will compare the request
# path against a hardcoded list of common scan targets from bots
# and attackers. If the path matches any of these targets, the
# IP will be blacklisted preemptively for the set time in minutes.
# You can disable it by setting it to `0`.
#
# default: 1440
# overwritten by: SUSPICIOUS_REQUESTS_BLACKLIST
blacklist = 1440

# Will emit a log with level of warning if a request to `/` has
# been made that has not been caught by any of the usual routes
# and handlers. Apart from a request to just `/` which will end
# in a redirect to `/auth/v1`, all additional path's will be
# logged. This can help to improve the internal suspicious
# blocklist in the future.
#
# default: false
# overwritten by: SUSPICIOUS_REQUESTS_LOG
log = false
</code></pre>
<p>At the time of writing, events for suspicious requests do not exist yet. This might change in the future.</p>
<h2 id="manual-blacklisting"><a class="header" href="#manual-blacklisting">Manual Blacklisting</a></h2>
<p>You can also manually blacklist an IP, either via the Admin UI or with an <a href="work/api_keys.html">API Key</a> with the correct
access rights. Just navigate to <code>Blacklist</code> in the Admin UI and click <code>Blacklist IP</code>.</p>
<h2 id="persistence"><a class="header" href="#persistence">Persistence</a></h2>
<p>The blacklist currently is in-memory only. This means you loose all blacklisted IPs when Rauthy restarts, if you opt-in
to <code>cluster.cache_storage_disk = false</code>.</p>
<p>The reason behind this is that blacklisting usually happens in scenarios under attack, when you want to do as little
work as possible, for instance to not end up with a DoS. The blacklisting middleware is also the very first one in
the API stack, even before access logging, to make sure Rauthy has the least amount of work blocking blacklisted IP's.</p>
<h2 id="expiry"><a class="header" href="#expiry">Expiry</a></h2>
<p>After a blacklisting expires, the entry will be fully removed from Rauthy and you will not see it anymore.</p>
<h2 id="blacklist-events"><a class="header" href="#blacklist-events">Blacklist Events</a></h2>
<p>You may or may not be notified about different blacklisting events. All auto-blacklisting will trigger an event.
The levels change depending on the failed login counter. You can adopt the levels to your likings by setting:</p>
<pre><code class="language-toml">[events]
# The level for the generated Event after certain
# amounts of false logins from an IP
#
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_25
level_failed_logins_25 = 'critical'
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_20
level_failed_logins_20 = 'critical'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_15
level_failed_logins_15 = 'warning'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_10
level_failed_logins_10 = 'warning'
# default: notice
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_7
level_failed_logins_7 = 'notice'
# default: info
# overwritten by: EVENT_LEVEL_FAILED_LOGIN
level_failed_login = 'info'
</code></pre>
<p>The event level for all other blacklisting can be set with this value:</p>
<pre><code class="language-toml">[events]
# The level for the generated Event after an IP has
# been blacklisted
#
# default: warning+
# overwritten by: EVENT_LEVEL_IP_BLACKLISTED
level_ip_blacklisted = 'warning'
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="json-web-keys"><a class="header" href="#json-web-keys">JSON Web Keys</a></h1>
<p>JSON Web Keys (JWKs) are used to sign JWT tokens. Multiple JWKs form a JSON Web Key Set (JWKS). These are private
/ public key pairs Rauthy generates internally in a cryptographically secure way. All of them are generated at
once, one for each algorithm Rauthy supports signing tokens with, which are</p>
<ul>
<li>RS256</li>
<li>RS384</li>
<li>RS512</li>
<li>EdDSA</li>
</ul>
<p>The <strong>RSA</strong> algorithms exist for compatibility. The <code>RS256</code> is the only mandatory algorithm by the OIDC RFC and
the <code>RS384</code>and <code>RS512</code> basically come for free, when you implement <code>RS256</code>. However, these algorithms, produce
pretty big signatures and are very slow to generate.</p>
<p>Rauthy does not support for <strong>ECDSA</strong> keys, because they are the worst option in this scenario in my opinion.
<code>ECDSA</code> keys have the advantage that they produce way smaller signatures than <code>RSA</code> keys and can be generated pretty
fast, but are slow at token validations. Tokens need to be validated with each single request, so you want to this
to be as fast as possible (without sacrificing security of course).</p>
<p>The best option is <strong>EdDSA</strong>, which uses <code>ed25519</code> keys. It is the fastest option at signing and validation, fast to
generate and produces the smallest signatures and therefore total token size. These are the default when you create
a new client, but some applications do not support them. If you have trouble logging in because of a problem with the
signature, try to change it to <code>RS256</code>.</p>
<h2 id="key-rotation-1"><a class="header" href="#key-rotation-1">Key Rotation</a></h2>
<p>As long as your private key's do not leak, you technically do not need to rotate keys and generate new ones.
However, it is good practice to do this regularly. Usually, you don't need to care about this. Rauthy rotates its JWKS
<strong>automatically</strong> with default settings. This will happen on each 1. of the month at 03:30 in the morning.<br />
You can change this behavior with the following setting:</p>
<pre><code class="language-toml">[lifetimes]
# JWKS auto rotate cronjob. This will (by default) rotate all JWKs every
# 1. day of the month. If you need smaller intervals, you may adjust this
# value. For security reasons, you cannot fully disable it.
# In a HA deployment, this job will only be executed on the current cache
# leader at that time.
# Format: "sec min hour day_of_month month day_of_week year"
#
# default: "0 30 3 1 * * *"
# overwritten by: JWK_AUTOROTATE_CRON
jwk_autorotate_cron = '0 30 3 1 * * *'
</code></pre>
<p>If you however had a secret's leak or something like this, you can of course rotate <strong>manually</strong>. You just need to log
in to the Admin UI:<br />
<code>Config</code> -&gt; <code>JWKS</code> -&gt; <code>Rotate Keys</code></p>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/jwks.html#admonition-info"></a>
</div>
<div>
<p>When Rauthy does a key rotation, the old keys will not be deleted immediately, because they will be needed for some
additional time to verify existing tokens.</p>
<p>A rotation generates new keys which will then be used for any upcoming token signatures. The old keys will be
auto-deleted after 30 days, you don't need to care about cleanup.</p>
</div>
</div>
<h2 id="rotation-event"><a class="header" href="#rotation-event">Rotation Event</a></h2>
<p>Rauthy creates a new event when keys are rotated. The default level is <code>notice</code>. If you want to change this,
you can do so with:</p>
<pre><code class="language-toml">[events]
# The level for the generated Event after the JWKS has
# been rotated
#
# default: notice
# overwritten by: EVENT_LEVEL_JWKS_ROTATE
level_jwks_rotate = 'notice'
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="i18n"><a class="header" href="#i18n">I18n</a></h1>
<p>Rauthy supports translated views for all user-facing UI and E-Mails.</p>
<p>At the time of writing, there are english (default) and german. If you need another language, it can be implemented
pretty easily, as the project is prepared for that. You can either open a PR and provide all the necessary translations,
or if you are a company, you can reach out to me: <code>mail@sebadob.dev</code>.</p>
<p>What will never happen though is the implementation of an automated approach, because all these generated translations
are usually very bad. Also, when the migration to Leptos has been done at some point, adding new translations will get
easier, as I will reorganize the way it's done. Offline preparations have been made already.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="user-logout"><a class="header" href="#user-logout">User Logout</a></h1>
<p>User logout can mean quite a few different things and there are multiple ways for logging out users.</p>
<h2 id="user-initiated-logout"><a class="header" href="#user-initiated-logout">User Initiated Logout</a></h2>
<p>This is the most straight forward logging out. If a user (or an admin in the Admin UI) clicks the logout button and
confirms the logout, the associated Rauthy session will be logged out. This means, to access any Rauthy UI's again, the
user needs to do a fresh login.</p>
<p>If you have OIDC Backchannel Logout (mentioned below) configured for any clients, this will trigger it for the Session
ID <code>sid</code> only. This means only clients where the user logged in in the past that are associated with this <code>sid</code> will
receive a Logout Token. All others will not. This is NOT a global user logout, only for this very session.</p>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="work/logout.html#admonition-caution"></a>
</div>
<div>
<p>The User Initiated Logout does NOT affect already signed and issue stateless(!) tokens. A stateless token, once singned,
cannot be revoked by design, even though you might read things like this in many places on the internet.</p>
<p>It is impossible to revoke a stateless JWT token.</p>
</div>
</div>
<h2 id="admin-forced-user-logout"><a class="header" href="#admin-forced-user-logout">Admin-Forced User Logout</a></h2>
<p>An admin can of course force-logout users via the Admin UI as well. This can be done in 2 places:</p>
<ul>
<li>Admin UI -&gt; Sessions -&gt; click delete on a specific session or <code>Invalidate All Sessions</code></li>
<li>Admin UI -&gt; Users -&gt; select user -&gt; Logout Tab -&gt; Logout</li>
</ul>
<p>The first session-targeted logout / invalidation will only trigger Backchannel Logouts for this specific session only.
If you log out a user via the Logout Tab for a selected user, it will be a global User Logout everywhere. This will
delete all existing sessions on all devices, delete all refresh tokens and trigger a Backchannel Logout for the whole
user for all configured clients.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/logout.html#admonition-note"></a>
</div>
<div>
<p>The global, forced user logout does NOT affect logged in devices via the device flow, which are usually either IoT
devices or maybe a CLI tool on a headless server. If you also want to remove access for all of these, do this
additionally via the Admin UI -&gt; Users -&gt; select user -&gt; Devices Tab.</p>
</div>
</div>
<h2 id="rp-initiated-logout"><a class="header" href="#rp-initiated-logout">RP Initiated Logout</a></h2>
<p>An RP Initiated Logout can be done by any client, that has a valid <code>id_token</code> for a given user. This logout provides
the best UX and makes it possible to redirect the user back to the downstream client afterward instead of Rauthy itself.</p>
<p>To make this possible, you first need to make sure, that you set a proper <code>Redirect URI</code> for your client in the Admin
UI -&gt; Client Config. Only URLs added here upfront will be allowed for the automated redirect. This logout procedure also
does not ask for Logout validation and the whole process works without any user interaction.</p>
<p>If you added a redirect URI, you can do this logout in 2 ways:</p>
<ul>
<li>Use the users browser, for instance after clicking a <em>Logout</em> button in the downstream client app</li>
<li>Do it in the background without the user knowing</li>
</ul>
<p>Via the browser is pretty simple:</p>
<ul>
<li>Redirect the user to Rauthy's logout endpoint, which is <code>/auth/v1/oidc/logout</code> and provide a valid <code>id_token</code> via the
<code>id_token_hint</code> URL parameter. If this contains a valid <code>id_token</code>, Rauthy will not ask for Logout confirmation and
it will accept an optionally given, valid redirect URI.</li>
<li>Add the <code>post_logout_redirect_uri</code> param and provide a URI, that has been allowed for this client upfront to avoid
open redirects.</li>
<li>You may also add a <code>state</code> parameter if you need to provide any information back to your client. This <code>state</code> will be
appended to the <code>post_logout_redirect_uri</code>, if it exists.</li>
</ul>
<p>Via the backend without user interaction, an RP can initiate a logout in the same way. This a lot simpler though. You
only would do a <code>POST /auth/v1/oidc/logout?id_token_hint=pasteAValidIdTokenHere</code>. It will work in the exact same way
with the exception, that you of course don't need any redirect or state, because your backend would do this request
directly.</p>
<p>In both situations, you at least need a valid <code>id_token</code> which has been returned in the past via the <code>/token</code> endpoint
after a successful login or token refresh.</p>
<p>The <em>RP Initiated Logout</em> will always trigger a Backchannel Logout on configured clients that are associated to the
<code>sid</code> from the <code>id_token</code>.</p>
<h2 id="backchannel-logout---client-side"><a class="header" href="#backchannel-logout---client-side">Backchannel Logout - Client Side</a></h2>
<p>A Backchannel Logout is sent from Rauthy to your downstream client application, if you configured it to do so. You can
configure it in the <code>Admin UI -&gt; Clients -&gt; select your client -&gt; Backchannel Logout</code>.</p>
<p>Either your client application already
supports <a href="https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Backchannel Logout</a>, or you might need to
implement it on your own. Accepting these logout requests is pretty straight forward, if you just take a look at the
RFC.</p>
<p>The <code>rauthy-client</code> will provide all the necessary tools for this in a future version as well.</p>
<h2 id="backchannel-logout---rauthys-side"><a class="header" href="#backchannel-logout---rauthys-side">Backchannel Logout - Rauthy's Side</a></h2>
<p>An <a href="https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Backchannel Logout</a> can be done as well against
Rauthy. You usually never implement it yourself though. Rauthy only accepts these backchannel logouts from configured
Upstream Auth Providers. These will provide a <code>logout_token</code> on the logout endpoint, which Rauthy will validate and on
success, forward the backchannel logout to all configured and associated clients.</p>
<p>You have quite a few config options regarding Backchannel Logout. All these values have reasonable defaults, but you
might take a look at them if you have issues like e.g. an upstream provider with self-signed certificates or inside
test environments.</p>
<pre><code class="language-toml">[backchannel_logout]
# The maximum amount of retries made for a failed backchannel logout.
# Failed backchannel logouts will be retried every 60 - 90 seconds
# from all cluster nodes. The timeout between retries is randomized
# to avoid overloading clients. It will be executed on each cluster
# member to increase the chance of a successful logout in case of
# network segmentations.
#
# default: 100
# overwritten by: BACKCHANNEL_LOGOUT_RETRY_COUNT
retry_count = 100

# The lifetime / validity for Logout Tokens issued by Rauthy in
# seconds. These Logout Tokens are being generated during OIDC
# Backchannel Logout requests to configured clients. The token
# lifetime should be as short as possible and at most 120 seconds.
#
# default: 30
# overwritten by: LOGOUT_TOKEN_LIFETIME
token_lifetime = 30

# You can allow a clock skew during the validation of Logout Tokens,
# when Rauthy is being used as a client for an upstream auth
# provider that uses backchannel logout.
#
# The allowed skew will be in seconds and a value of e.g. 5 would
# mean, that 5 seconds are added to the `iat` and `exp` claim
# validations and expand the range.
#
# default: 5
# overwritten by: LOGOUT_TOKEN_ALLOW_CLOCK_SKEW
allow_clock_skew = 5

# The maximum allowed lifetime for Logout Tokens. This value is
# a security check for upstream auth providers. If Rauthy
# receives a Logout Token, it will check and validate, that the
# difference between `iat` and `exp` is not greater than
# `allowed_token_lifetime`. This means Rauthy will reject Logout
# Tokens from clients with a way too long validity and therefore
# poor implementations. The RFC states that tokens should be
# valid for at most 120 seconds.
#
# default: 120
# overwritten by: LOGOUT_TOKEN_ALLOWED_LIFETIME
allowed_token_lifetime = 120
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="scim"><a class="header" href="#scim">SCIM</a></h1>
<h2 id="tldr"><a class="header" href="#tldr">tl;dr</a></h2>
<p>Rauthy requires:</p>
<ul>
<li>the following SCIM v2 endpoints under <code>{scim_base_uri}</code>:
<ul>
<li><code>GET /Users?filter=...</code></li>
<li><code>POST /Users</code></li>
<li><code>GET /Users/{id}</code></li>
<li><code>PUT /Users/{id}</code></li>
<li><code>DELETE /Users/{id}</code> with <code>scim.sync_delete_users = true</code></li>
<li><code>GET /Groups?filter=...</code></li>
<li><code>POST /Groups</code></li>
<li><code>GET /Groups/{id}</code></li>
<li><code>PATCH /Groups/{id}</code> with <code>PatchOp</code></li>
<li><code>DELETE /Groups/{id}</code> with <code>scim.sync_delete_groups = true</code></li>
</ul>
</li>
<li>the client to handle <code>externalId</code> properly</li>
<li><code>fitler=externalId</code> on both <code>{base_url}/Users</code> and <code>{base_url}/Groups</code></li>
<li><code>filter=userName</code> on <code>{base_url}/Users</code></li>
<li><code>filter=displayName</code> on <code>{base_url}/Groups</code></li>
<li><code>userName</code> to be set properly to the users <code>email</code> to match against the DB</li>
<li><code>display</code> to be set properly for <code>ScimGroup</code>s to match against the DB</li>
<li><code>PatchOp</code>s to be working properly on <code>PATCH {base_url}/Groups/{id}</code>
<ul>
<li><code>replace</code> will be used for updating the group itself</li>
<li><code>add</code> / <code>remove</code> ops will be used to modify membership</li>
</ul>
</li>
</ul>
<h2 id="scim-requests"><a class="header" href="#scim-requests">SCIM Requests</a></h2>
<p>This is just a short section about the SCIM flow Rauthy uses. If you only care about an implementation with Rauthy, you
don't need to implement the full SCIM RFC.</p>
<p>As an example, we will go through what happens, when you add SCIM v2 to a client. This will trigger a complete sync
which involves all steps. SCIM syncs are also triggered for instance when updating a single user or a group name, but
these are sub sets of what will happen during a full client sync. So, let's assume you add a SCIM v2 base URL with
enabled Groups sync to a client. It will then do the following requests to the client:</p>
<ol>
<li><code>GET {base_url}/Users</code> with first <code>filter=externalId</code> and if nothing was found, with <code>filter=userName</code>
<ol>
<li>If the User exists and should not be synced because of a group prefix filter, <code>DELETE {base_url}/Users/{id}</code>
(<code>scim.sync_delete_users</code>)</li>
<li>If the user does not exist on the client, <code>POST {base_url}/Users</code> and create the new user</li>
<li>If it was found by the <code>GET</code>, update all values and make sure <code>externalId</code> is set properly, if it has been found
by <code>userName</code></li>
</ol>
</li>
<li>Check if Rauthy should sync groups for this client, and skip groups if not
<ol>
<li>Get all remote groups via  <code>GET {base_url}/Groups</code> with optional <code>filter=externalId</code> or <code>filter=displayName</code></li>
<li>Compare the <code>user.groups</code> from remote against the assignments on Rauthys side</li>
<li>Only delete groups on remote if they have a matching <code>externalId</code> and should be deleted according to Rauthys
config (<code>scim.sync_delete_groups</code>)</li>
<li>If groups don't exist on remote while they should, <code>POST {base_url}/Groups</code> to create them</li>
<li>If a match was found by <code>externalId</code> with an outdated name, <code>PATCH {base_url}/Groups{id}</code> to update it properly</li>
</ol>
</li>
<li>When Users and Groups exist like they should, make sure Groups assignments are correct. These are done via <code>PatchOp</code>s
to <code>PATCH {base_url}/Groups/{id}</code></li>
</ol>
<p>These steps cover everything that could happen, apart from a group name update. If this happens, Rauthy will do a
<code>PATCH {base_url}/Groups{id}</code> with the <code>replace</code> setting to replace the groups name / <code>externalId</code>.</p>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="work/scim.html#admonition-info"></a>
</div>
<div>
<p>If <code>scim.sync_delete_groups</code> / <code>scim.sync_delete_users</code> is disabled, Rauthy will not delete the resources on remote, it
will only remove the <code>externalId</code> value to indicate, that this resource is not managed by Rauthy anymore. This is
important for instance in some scenarios, where you may have important data linked to a user, that you want to keep,
even if the user does not exist anymore.</p>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="forward-authentication"><a class="header" href="#forward-authentication">Forward Authentication</a></h1>
<p>If you want to secure applications running behind a reverse proxy that do not have authn / authz on their own in terms
of being able to use OIDC flows or similar with them, you can use something like the <a href="https://traefik.io/">Traefik</a>
middleware <a href="https://doc.traefik.io/traefik/middlewares/http/forwardauth/">ForwardAuth</a>, or Nginx <code>auth_request</code>. Other
proxies support this as well.</p>
<p>Incoming requests can be intercepted and forwarded to Rauthy first. It will check for a valid <code>Bearer</code> token in the
<code>Authorization</code> header. If it is valid, Rauthy will return an <code>HTTP 200 Ok</code> and will append additional headers with
information about the user to the request. These headers could easily be read by a downstream application.</p>
<p>The forward auth request can optionally add forward auth headers upon success. These will provide information about the
logged-in user to downstream applications. If your client app cannot make use of them, leaving them disabled is the best
idea. This is also the default, because they can leak information, if you do not set up your internal environment
carefully. You can enable the feature with</p>
<pre><code class="language-toml">[auth_headers]
# You can enable authn/authz headers which would be added to the
# response of the `/auth/v1/oidc/forward_auth` endpoint. When set to
# `true`, the headers below will be added to authenticated requests.
# These could be used on legacy downstream applications, that don't
# support OIDC on their own.
#
# However, be careful when using this, since this kind of authn/authz
# has a lot of pitfalls out of the scope of Rauthy.
#
# default: false
# overwritten by: AUTH_HEADERS_ENABLE
enable = false
</code></pre>
<p>You can also change the header names containing the information, if you need to support some older application that
needs special naming for them:</p>
<pre><code class="language-toml">[auth_headers]
# Configure the header names being used for the different values. You
# can change them to your needs, if you cannot easily change your
# downstream apps.
#
# default: x-forwarded-user
# overwritten by: AUTH_HEADER_USER
user = 'x-forwarded-user'
# default: x-forwarded-user-roles
# overwritten by: AUTH_HEADER_ROLES
roles = 'x-forwarded-user-roles'
# default: x-forwarded-user-groups
# overwritten by: AUTH_HEADER_GROUPS
groups = 'x-forwarded-user-groups'
# default: x-forwarded-user-email
# overwritten by: AUTH_HEADER_EMAIL
email = 'x-forwarded-user-email'
# default: x-forwarded-user-email-verified
# overwritten by: AUTH_HEADER_EMAIL_VERIFIED
email_verified = 'x-forwarded-user-email-verified'
# default: x-forwarded-user-family-name
# overwritten by: AUTH_HEADER_FAMILY_NAME
family_name = 'x-forwarded-user-family-name'
# default: x-forwarded-user-given-name
# overwritten by: AUTH_HEADER_GIVEN_NAME
given_name = 'x-forwarded-user-given-name'
# default: x-forwarded-user-mfa
# overwritten by: AUTH_HEADER_MFA
mfa = 'x-forwarded-user-mfa'
</code></pre>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="work/forward_auth.html#admonition-caution"></a>
</div>
<div>
<p>This feature makes it really easy for any application behind your reverse proxy to serve protected resources, but you
really only use it, if you cannot use a proper OIDC client or something like that.</p>
<p>Rauthy tries to catch as many config errors as possible, but a lot of issues can happen outside of its scope. Forward
Auth has many pitfalls and you need to be very careful with your whole setup when using it. If you only have a tiny
configuration error in your reverse proxy or your whole environment, it's easily possible that all your security is gone
immediately. For instance, configuring the HTTP method forwarded to Rauthy wrongly could disable all CSRF checks, or if
there is a loopwhole in your setup and it's possible for someone to access your client app avoiding your reverse proxy,
it would be the same as if there was not forward auth at all.</p>
<p>A direct token validation inside your downstream application via <strong>a native OIDC client should ALWAYS be preferred!</strong>
Use Forward Auth only as a last resort!</p>
</div>
</div>
<h2 id="different-types"><a class="header" href="#different-types">Different Types</a></h2>
<p>Rauthy provides 2 different types of Forward Auth. The first one is very basic and simple and in 99.9% of cases not
what you want. The second one is way more advances, but requires a more complicated setup.</p>
<h2 id="simple-forward-auth"><a class="header" href="#simple-forward-auth">Simple Forward Auth</a></h2>
<p>The simple Forward Auth expects a valid JWT token created by Rauthy in the <code>Authorization</code> header. That's it. This type
of Forward Auth exists mostly for compatibility reasons and is not what you want most of the time. It can work with
Traefik's middleware for instance, but you need to have some way to provide a JWT token, which usually requires manual
work or changing something about the client. The endpoint for this simple forward auth is:</p>
<pre><code>/auth/v1/oidc/forward_auth
</code></pre>
<h2 id="advanced-forward-auth"><a class="header" href="#advanced-forward-auth">Advanced Forward Auth</a></h2>
<p>This second type is way more involved, but also a more secure. You need a more complicated reverse proxy setup, but if
Forward Auth is the only way for you to secure a legacy app, always use this type!</p>
<p>This book currently has examples for Traefik and nginx. If anyone wants to provide configuration examples for other
proxies, feel free to submit a PR.</p>
<h3 id="tldr-1"><a class="header" href="#tldr-1">tl;dr</a></h3>
<ul>
<li>Have a reverse proxy in front of the application you want to protect, like e.g. Traefik or nginx</li>
<li>Make sure it's impossible(!) to access the client app by skipping the reverse proxy, e.g. with network policies,
internal networks, or something like that.</li>
<li>Find an unsued route on your client app, where you can inject Rauthys <code>/callback</code>. You will probably be fine with
something like <code>/oidc/rauthy/callback</code>. The name does not matter, it just must not be used by the app already.</li>
<li>Inject Rauthy special <code>/callback</code> endpoint into your client app with the help of your reverse proxy. This is a step
that most other IdP's skip for a less secure setup. Rauthy does not let you skip this.</li>
<li>Configure your reverse proxy properly, so it forwards each single request to your client app, or the routes that
should be protected, to Rauthy, so that all requests are being validated.</li>
<li>Have a Client configured on Rauthy with the following settings:
<ul>
<li>must have at least one allowed origin configured that matches the origin of the location where it will be injected
into</li>
<li>the first configured <code>redirect_uri</code> must be absolute (not end with a wildcard *) - the redirect to the <code>/callback</code>
will be created from the first allowed <code>redirect_uri</code> automatically to have another safety hook and prevent misuse
on proxy level</li>
<li>must not have PKCE enabled, because it's an unnecessary overhead when Rauthy does a request against "itself"</li>
</ul>
</li>
</ul>
<h3 id="how-it-works"><a class="header" href="#how-it-works">How it works</a></h3>
<p>To make it work, you will have 2 endpoints:</p>
<ul>
<li><code>/auth/v1/clients/{id}/forward_auth</code></li>
<li><code>/auth/v1/clients/{id}/forward_auth/callback</code></li>
</ul>
<p>The <code>/auth/v1/clients/{id}/forward_auth</code> is used to process the auth requests. It reads a few different headers that are
injected by the proxy:</p>
<ul>
<li><code>X-Forwarded-For</code> or <code>X-Real-Ip</code></li>
<li><code>X-Forwarded-Proto</code></li>
<li><code>X-Forwarded-Host</code></li>
<li><code>X-Forwarded-Method</code></li>
<li><code>X-Forwarded-URI</code> or <code>X-Original-URL</code></li>
</ul>
<p>The additional <code>X-Original-URL</code> is a duplicate and only exists because it's mentioned in many guides. However, Rauthy
accepts either <code>X-Forwarded-URI</code> or <code>X-Original-URL</code> and checks both of them to provide more compatibility. You can
provide any of them.</p>
<p>The <code>X-Forwarded-Proto</code> and <code>X-Forwarded-Host</code> will be merged into the final <code>Origin</code>, that would be provided by the
browser if it was a direct request. This is then matched against the <code>allowed_origin</code>s configuration for the client to
have an additional safety hook in case of proxy misconfiguration or exposing the wrong forward auth on the wrong route.
Depending on <code>X-Forwarded-Method</code>, Rauthy will do an additional CSRF check. It does 2 of them:</p>
<ul>
<li>Check the additional <code>strict</code> cookie and match the included, encrypted CSRF token value to the one from the also
encrypted session cookie</li>
<li>Make sure that the <code>sec-fetch-site</code> is either <code>none</code> or <code>same-origin</code></li>
</ul>
<p>Usually, one of the methods would already be sufficient. Checking both of them is an additional defense in depth. If you
use a somewhat up-to-date browser, this will secure you against CSRF.</p>
<p>Unlike the simple <code>forward_auth</code> above, this endpoint checks for an encrypted session cookie. This is much more secure,
but requires more setup. The session will be bound to the already running Rauthy session, so you can revoke it any time.</p>
<p>The forward auth headers will only be added, if they are enabled. The endpoint works just fine with them being disabled.
It will then only authenticate the request and you can skip parsing and forwarding all those response headers to the
client, which will make the config way smaller.</p>
<p>The <code>danger_cookie_insecure</code> param will make the cookie insecure for local testing. This makes it possible to test via
plain HTTP, which you of course should never set in prod. You have a second param <code>redirect_state</code> you can provide to
change the status code, if no session could be found. By default, it returns a <code>401</code>, which is why nginx in that case
extracts the <code>location</code> header and returns it to the user, which will redirect to the <code>/authorize</code> page. Traefik however
would need a <code>302</code> here. The request params allow you to adjust the behavior depending on your reverse proxy.</p>
<p>To make all of this work with secure cookies, (Rauthy sets <code>__Host-*</code> cookies for max security (without the
<code>danger_cookie_insecure</code>), you need to inject the callback URI into your client app with the reverse proxy.</p>
<p>The callback endpoint needs the following headers being forwarded:</p>
<ul>
<li><code>X-Forwarded-For</code> or <code>X-Real-Ip</code></li>
<li><code>X-Forwarded-Proto</code></li>
<li><code>X-Forwarded-Host</code></li>
</ul>
<p>It will then, depending on the also encrypted <code>state</code> param from the <code>/authorize</code>, create new session cookies for that
injected domain which will be bound to the original Rauthy session created during the login. Afterward, it will
redirect to the original URI of the request before the forward auth failed for best UX <code>X-Original-URI</code> /
<code>X-Forwarded-URI</code>.</p>
<p>Rauthy will very strictly check and compare all headers against the configured client. It does not extract headers
dynamically to "just use them", to avoid issues like for instance re-using the wrong Forward Auth in an unintended
location, because reverse proxy configs can become very complex pretty quickly. Forwarding these headers (mentioned
below) is absolutely crucial for the security of this setup!</p>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="work/forward_auth.html#admonition-hint"></a>
</div>
<div>
<p>If you end up in an infinite redirect loop later on, you most probably have an issue with your Cookies not being set
correctly. This can happen for instance, when you are testing in an insecure environment, but having not told Rauthy
to create insecure cookies, so they would get rejected by the browser on the <code>/callback</code> endpoint.</p>
</div>
</div>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/forward_auth.html#admonition-note"></a>
</div>
<div>
<p>Both example configs for reverse proxies down below re-use Rauthy's own <code>/auth/v1/whoami</code> route for demonstration,
instead of a separate client app. This endpoint helps you debugging your setup, if you have issues with headers maybe.
You can enable <code>access.whoami_headers = true</code> to make it also return all found headers.</p>
<p>The setup works in the exact same way though for a separate client application.</p>
</div>
</div>
<h3 id="why-is-this-secure"><a class="header" href="#why-is-this-secure">Why is this secure?</a></h3>
<p>The cookies are (by default) <code>__Host-*</code> cookies with all the necessary, hardened cookie settings. The session cookie is
<code>same-site: lax</code> to make routing possible and the additional CSRF check cookie is <code>same-site: strict</code>.</p>
<p>To harden these cookies that much, the <code>/callback</code> must be injected into the exact same domain / host as the client app.
This means you need to find a route that does not overlap. Probably something like <code>/callback</code> or <code>/oidc/callback</code>
should always be unused, when you client app does not support any of these mechanisms in the first place. You could also
do something very unlikely overlapping things like <code>/oidc/rauthy/fwd_auth/callback</code> or whatever makes sense in your
case. This does not matter, only the host, so the browser sets the cookie correctly.</p>
<h3 id="prerequisites"><a class="header" href="#prerequisites">Prerequisites</a></h3>
<ol>
<li>have your reverse proxy in place</li>
<li>have you client app up and running</li>
<li>have Rauthy up and running</li>
<li>know a route you can overwrite on your client app, where you can inject Rauthys <code>/callback</code></li>
<li>have a client on Rauthy:
<ul>
<li>Allowed Origins MUST point to the Origin of your client app</li>
<li>The first Redirect URI MUST be absolute and point to the injected <code>/callback</code> from point 4</li>
<li>disable both plain and S256 PKCE flows</li>
</ul>
</li>
</ol>
<h3 id="nginx"><a class="header" href="#nginx">nginx</a></h3>
<p>When using nginx, you need the <code>auth_request</code> module enabled. This should be the case by default usually in recent
container images and versions. The example config does not show the bare minimum, but also already includes some minor
optimizations for proxied requests. It also shows a testing setup on localhost and no HTTPS setup.</p>
<pre><code>upstream rauthy {
    server localhost:8080;
}

server {
    listen       8000;
    server_name  localhost;

    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

    proxy_http_version 1.1;
    proxy_cache_bypass $cookie_session;
    proxy_no_cache $cookie_session;
    proxy_buffers 4 32k;
    client_body_buffer_size 128k;

    send_timeout 5m;
    proxy_read_timeout 240;
    proxy_send_timeout 240;
    proxy_connect_timeout 240;

    # this nginx-internal, virtual endpoint makes the magic happen
    location = /forward_auth {
        internal;

        # These are the requiered, above mentioned request headers.
        # The `X-Forwarded-For` is inserted into every request globally 
        # above already.
        proxy_set_header X-Original-URI $request_uri;
        proxy_set_header X-Forwarded-Method $request_method;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-URI $request_uri;

        # We don't want to forward any body to Rauthy. This would be a 
        # huge waste of resources.
        proxy_set_header Content-Length "";
        proxy_set_header Connection "";
        proxy_pass_request_body off;

        # This line is the one that forwards the request to Rauthy.
        # The client used in this example url has the id `test`.
        #
        # NEVER set `danger_cookie_insecure=true` in production!
        # This is only used for testing purposes!
        #
        # Apart from `danger_cookie_insecure`, you can optionally pass in
        # `redirect_state` which will change the default `401` state if
        # validation failed and nginx should redirect. nginx can handle this
        # dynamically, but it exists for compatibility.
        proxy_pass http://rauthy/auth/v1/clients/test/forward_auth?danger_cookie_insecure=true;
    }

    # This is the callback endpoint, that you inject into your client app.
    # In this example, we use the same host, because we are using Rauthy's 
    # own /whoami endpoint for better debugging.
    #
    # To have less chance of conflicts with your client app, you could set it
    # to something like /oidc/rauthy/callback and adjust the Allowed Redirect URI
    # for the client you are using accordingly. The first redirect URI for the 
    # Rauthy client MUST point to exactly this location.
    location = /callback {
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $http_host;

        proxy_pass http://rauthy/auth/v1/clients/test/forward_auth/callback?$args;
    }

    # This is our protected "client app". In this example, we are re-using Rauthy's
    # own whoami endpoint for better debugging and so the example does not need
    # another deployment.
    location /whoami {
        auth_request /forward_auth;
        auth_request_set $redirection_url $upstream_http_location;
        error_page 401 =302 $redirection_url;

        # If you have auth headers disabled, you can skip these next 2 blocks,
        # which are basically just copying them from the Rauthy repsonse
        # into the request that is forwarded to the client app.
        
        ## Auth Headers Rauthy sends when `auth_headers.enable = true`
        auth_request_set $user $upstream_http_x_forwarded_user;
        auth_request_set $roles $upstream_http_x_forwarded_user_roles;
        auth_request_set $groups $upstream_http_x_forwarded_user_groups;
        auth_request_set $email $upstream_http_x_forwarded_user_email;
        auth_request_set $email_verified $upstream_http_x_forwarded_user_email_verified;
        auth_request_set $family_name $upstream_http_x_forwarded_user_family_name;
        auth_request_set $given_name $upstream_http_x_forwarded_user_given_name;
        auth_request_set $mfa $upstream_http_x_forwarded_user_mfa;

        ## Inject Auth Headers into the request to the client app
        proxy_set_header x-forwarded-user $user;
        proxy_set_header x-forwarded-user-roles $roles;
        proxy_set_header x-forwarded-user-groups $groups;
        proxy_set_header x-forwarded-user-email $email;
        proxy_set_header x-forwarded-user-email-verified $email_verified;
        proxy_set_header x-forwarded-user-family-name $family_name;
        proxy_set_header x-forwarded-user-given-name $given_name;
        proxy_set_header x-forwarded-user-mfa $mfa;

        proxy_pass http://rauthy/auth/v1/whoami;
    }
}
</code></pre>
<h3 id="traefik"><a class="header" href="#traefik">Traefik</a></h3>
<p>This Traefik example config is for an <code>IngressRoute</code> when Traefik is deployed inside K8s. It should be pretty similar,
if you run it in docker though. This example re-used Rauthy's own <code>/whoami</code> endpoint again for simplicity, but the
setup is the same for a dedicated client app.</p>
<pre><code class="language-yaml">apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: fwd-auth
spec:
  forwardAuth:
    # The id for the properly configured client is `fwd-auth-test` in this case.
    # Unlike nginx, Traefik also expects a `302` status code on unsuccessful authentication.
    # We are using Rauthy's K8s-internal address here, but it will work in the same way with 
    # a public one.
    address: http://rauthy.rauthy-dev.svc.cluster.local:8080/auth/v1/clients/fwd-auth-test/forward_auth?redirect_state=302
    # If you don't have auth headers enabled, you can remove this block.
    authResponseHeaders:
      - x-forwarded-user
      - x-forwarded-user-roles
      - x-forwarded-user-groups
      - x-forwarded-user-email
      - x-forwarded-user-email-verified
      - x-forwarded-user-family-name
      - x-forwarded-user-given-name
      - x-forwarded-user-mfa
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: fwd-auth-callback
spec:
  replacePath:
    # The id for the properly configured client is `fwd-auth-test` in this case.
    # By simply replacing the path to the correct one, we can freely choose any
    # location where we want to inject the callback URI into the client app.
    path: /auth/v1/clients/fwd-auth-test/forward_auth/callback
---
# You probably don't need this middleware when protecting a client app.
# It just exists for this example to route to Rauthys whoami endpoint properly.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: whoami-replace
spec:
  replacePath:
    path: /auth/v1/whoami
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: rauthy-https
  namespace: rauthy-dev
spec:
  entryPoints:
    - websecure
  tls:
    secretName: iam.sebadob.dev-tls
  routes:
    - match: Host(`iam.sebadob.dev`)
      kind: Rule
      services:
        - name: rauthy
          port: 8080
    # This is the route to your client app you want to protect.
    - match: Host(`iam.sebadob.dev`) &amp;&amp; Path(`/whoami`)
      kind: Rule
      middlewares:
        # CAUTION: very important to have the fwd-auth BEFORE any path rewriting.
        # You can set a fixed redirect URI when you overwrite `X-Forwarded-URL`.
        # Also, Traefik can mess up some headers if you have path rewriting applied.
        # If you have issues with this, make sure to check this.
        - name: fwd-auth
        - name: whoami-replace
      services:
        - name: rauthy
          port: 8080
    # This is the callback route you need to inject into your client app.
    - match: Host(`iam.sebadob.dev`) &amp;&amp; Path(`/callback`)
      kind: Rule
      middlewares:
        - name: fwd-auth-callback
      services:
        - name: rauthy
          port: 8080
</code></pre>
<h3 id="caddy"><a class="header" href="#caddy">Caddy</a></h3>
<p>This is a very simple Caddy config example to get you started:</p>
<pre><code>test.example.com {
    handle {
        forward_auth rauthy {
            uri /auth/v1/clients/test/forward_auth?redirect_state=302
        }
        reverse_proxy x.x.x.x:xx
    }
    handle /auth/v1/clients/* {
        reverse_proxy rauthy {
            header_up X-Forwarded-Method {method}
            header_up X-Forwarded-Uri {uri}
        }
    }
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="pam"><a class="header" href="#pam">PAM</a></h1>
<p>OIDC / OAuth covers almost all web apps, and for those that don't have any support, Rauthy comes with <code>forward_auth</code>
support. To not need an additional LDAP / AD / something similar for your backend and workstations, Rauthy comes with
its own custom PAM module. It does not just use JWT Tokens for logging in, but you can actually manage all your Linux
hosts, groups and users in different ways. You have the option to secure local logins to workstations via Yubikey
(only USB Passkeys supported, no QR-code / software keys), and all SSH logins can be done with ephemeral, auto-expiring
passwords, that you can generate via your Account dashboard, if an Admin has created a PAM user for you.
This means you basically have MFA-secured SSH logins without the need for any modifications or additional software on
your local SSH client, and you can use any SSH client from any machine securely, even if it's not your own.</p>
<p>Pam hosts, groups and users are managed via Rauthys Admin UI in the <code>PAM</code> section.</p>
<p>In addition to a PAM module, you get an NSS module and an NSS proxy that runs on each machine. You can dynamically
log in to any machine an Admin has given you access to. Users and groups are not added to local files, but will be
resolved via the network.</p>
<p>This module is published in a separate repo to avoid licensing issues, since it relies on some GPLv3 dependencies. You
can take a look at it here: <a href="https://github.com/sebadob/rauthy-pam-nss">rauthy-pam-nss</a>.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/pam.html#admonition-note"></a>
</div>
<div>
<p>You will notice on the following pages and when testing the PAM setup via the Admin UI, that quite a few resources and
values are immutable after their initial creation. The reason is simply to avoid things like accidentially leaking
left-over user data, when for instance a user was deleted and a new one with the exact same name was created, but maybe
some files are left on some hosts, and so on. The same is true for instance for group names and types, just to avoid
confusion for admins.</p>
</div>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="pam-groups"><a class="header" href="#pam-groups">PAM Groups</a></h1>
<p>PAM Groups are independent of the general OIDC groups. This is the case for all PAM resources, because Linux regexes for
validation are much stricter. PAM groups can also do different things, depending on their type.</p>
<p>PAM Groups are the first resource you need for a new setup, because they are the glue between hosts and users and
manage general access.</p>
<h2 id="wheel-rauthy"><a class="header" href="#wheel-rauthy"><code>wheel-rauthy</code></a></h2>
<p>This group is special. It will always exist, it will always have <code>gid</code> 100_000 and it cannot be deleted. Membership
for this group can also not be configured directly. Instead, it will be calculated dynamically for each user / host
combination depending on if the user is allowed to <code>wheel</code> in that particular host group. More on that later.</p>
<p>The idea of <code>wheel-rauthy</code> is that it should serve the same purpose as <code>wheel</code> on hosts, like e.g. allowing all members
in <code>wheel-rauthy</code> to use <code>sudo</code>. Theoretically, it could be merged with <code>wheel</code> on all hosts locally, but having a
distinct name provides more flexibility for configuration when you e.g. want to handle local users a bit differently
from Rauthy-managed ones.</p>
<h2 id="types"><a class="header" href="#types">Types</a></h2>
<p>You can create groups of 4 different types. Each type serves a specific purpose.</p>
<h3 id="host-groups"><a class="header" href="#host-groups"><code>host</code> groups</a></h3>
<p><code>host</code> groups manage access to hosts. Each PAM Host must be assigned to exactly a single host group. When you assign
a PAM User as a member in a host group, this user will be allowed to log in to all hosts in this specific group, and it
will be rejected when the user is not a member. In addition, when you also allow <code>wheel</code> for a user in a host group,
this user will be dynamically assigned to the <code>wheel-rauthy</code> group mentioned above, and can therefore be used for things
like access to <code>sudo</code> and such.</p>
<p>You might be used to manage hosts by domain. Rauthy does not care about in which domain a host is and instead has the
host groups. The reason is that host groups provide a more fine-grained way of managing access and are therefore more
flexible. You can have multiple host groups in the same domain, and e.g. name your groups after teams or projects, while
the hosts are maybe technically in the same domain.</p>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="work/pam_groups.html#admonition-hint"></a>
</div>
<div>
<p>If you want to delete a host group, you can only do this, if there is not a single host left that is assigned to this
group, to prevent invalid setups. You will only see the delete button, if the host group has no host assigned. If users
are still assigned, the relation will simply be deleted.</p>
</div>
</div>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/pam_groups.html#admonition-note"></a>
</div>
<div>
<p>You will not see <code>host</code> groups in NSS lookups, like when you do <code>getent group</code>. They only exist for managing access
to hosts. All other group types will be resolved on hosts like you would expect it.</p>
</div>
</div>
<h3 id="generic-groups"><a class="header" href="#generic-groups"><code>generic</code> groups</a></h3>
<p><code>generic</code> groups can be seen as a counterpart to your groups from <code>/etc/group</code>. The exception is, that they don't exist
locally and will be resolved via the NSS module. The important thing about them is, that their <code>gid</code> is globally unique,
not just on a host or inside a host group. This means if you e.g. have an NFS share and manage permissions via <code>gid</code> of
a generic group, it will be the exact same value with the exact same access on each host where you mount it.</p>
<h3 id="local-groups"><a class="header" href="#local-groups"><code>local</code> groups</a></h3>
<p><code>generic</code> groups are almost the same, but when your <code>/etc/nsswitch.conf</code> is configured correctly, the <code>rauthy-nss</code> proxy
will merge them with groups that might exist locally in <code>/etc/group</code> on the hosts. This is for instance important for
situations like giving a user access to <code>docker</code>, which typically requires them to be a member of the <code>docker</code> group.
To achieve this, create a <code>local</code> group with the name <code>docker</code>, and the results will be merged and access granted.
Their <code>gid</code> is globally unique on Rauthy, but the NSS proxy will overwrite it on each host individually with the local
<code>gid</code> from the <code>/etc/group</code> entry.</p>
<h3 id="user-groups"><a class="header" href="#user-groups"><code>user</code> groups</a></h3>
<p>You cannot create these groups manually. When a new user is created, a <code>user</code> group with the same name as the username
will be created, just like you know if from any Linux system. You can assign other users to existing user groups, e.g.
when users want to be able to share certain files on hosts, but apart from that, they are managed automatically. Their
lifetime is bound to the users' lifetime. They provide the same guarantees as <code>generic</code> groups. Their <code>gid</code> will be
globally unique everywhere.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="pam-hosts"><a class="header" href="#pam-hosts">PAM Hosts</a></h1>
<p>PAM Hosts are usually the 2nd resource you need, when creating a fresh setup. A default installation of Rauthy will
automatically come with a <code>host</code> group named <code>default</code> to get you started, but you can delete it and create your own,
and of course additional ones to group your hosts properly. However, at least a single host group must exist, before
you can create your first PAM Host. The hostname you provide must follow the Linux rules for hostnames.</p>
<p>PAM Hosts should be pretty much self-explanatory. Each one must have a unique hostname and only a single host group
assignment to manage access for users. You can set multiple IPs and aliases for a host, and they will show up in NSS
lookups, like e.g. <code>getent hosts</code>. They are basically an extension to your <code>/etc/hosts</code> file and (if configured
correctly), they will overwrite DNS resolutions, or provide a Hostname -&gt; IP resolve even if no DNS exists, just like
your <code>/etc/hosts</code> file.</p>
<h2 id="force-mfa"><a class="header" href="#force-mfa">Force MFA</a></h2>
<p>This setting can be used to further lock down access to a host, in addition to membership in a host group. If set, only
accounts that are MFA secured are allowed to log in to this host. A local login would request a Passkey. For logins via
ssh, you need to generate a Remote PAM Password in your account dashboard and only when generated from a user session
that is MFA-secured, you can log in.</p>
<h2 id="local-password-login"><a class="header" href="#local-password-login">Local Password Login</a></h2>
<p>This option is a bit special to solve a niche issue. If you have users that should be allowed to log in locally to
workstations, and they don't have USB Passkeys (which are the only ones that actually make sense), but instead something
that generates a QR code via smartphone, or Passkeys stored in a password manager (seriously, don't do that...), you can
set this option.</p>
<p>If this is set, a local login will NEVER request a passkey, even if the account is MFA secured. Instead, it will only
request the users' password. The reason is, that most software passkeys will simply not work for local logins,
especially when you log in to a workstation with a window manager, where you cannot even show a QR code easily. USB
Passkeys will still be fine and you will get a prompt like <code>Passkey PIN</code> instead of the usual <code>Password</code> and you should
leave this option disabled, if all your users that need local access have USB Passkeys.</p>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/pam_hosts.html#admonition-note"></a>
</div>
<div>
<p>If you log in to a window manager locally and have a MFA secured account, and Local Password Login is NOT set, you
usually see <code>Passkey PIN</code>. This is however not the case for all window managers. For instance, <code>sddm</code> ignores this
prompt from the PAM module and overwrites it with <code>Password</code>. If this is the case and you have a Passkey linked to your
account, you need to enter your PIN at that point and touch your key. Sometimes, they don't forward the prompt properly.</p>
</div>
</div>
<h2 id="ips-and-aliases"><a class="header" href="#ips-and-aliases">IPs and Aliases</a></h2>
<p>Whatever you provide as values here can be reverse-resolved on hosts via NSS lookups. E.g. when you have a host with the
name <code>tux</code>, add an IP <code>192.168.1.100</code> and an alias of <code>host1.example.local</code>, you can directly do things like
<code>ping tux</code> or <code>curl host1.example.local</code>, and as long as everything is configured correctly, these requests can resolve
to the IP <code>192.168.1.100</code> without any additional DNS servers.</p>
<h2 id="notes"><a class="header" href="#notes">Notes</a></h2>
<p>The notes field is just for cases when you want to write some notes for all users that can log in to this hosts, like
e.g. mention some special directory or things like that.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="pam-users"><a class="header" href="#pam-users">PAM Users</a></h1>
<p>A PAM User is always linked to a "real" user and after it was created, it is immutable and automatically bound to the
lifetime of the "real" user. You cannot change the <code>username</code> afterward or even delete it and create a new one easily.
The reason is the same as for the immutable group names, to never accidentally leak some left-over files. There is also
typically no need to ever change these names, as it would just be confusing anyway.</p>
<p>A <code>uid</code> is globally unique over all hosts, just as <code>gid</code>s for groups. This means you can rely on it even for managing
NFS shares and things like that. The only value you can change for a user after the creation is the <code>shell</code>, but this
will just stay the default <code>/bin/bash</code> in probably almost all cases anyway.</p>
<p>The more important section for a user is the groups membership. Groups are separated by type, and these types are
explained in the <a href="work/pam_groups.html#types">PAM Groups</a> doc. At this point, there should be not open questions about them.</p>
<p>PAM Users are pretty simple. The most important thing about them is their additional <code>username</code>, which must follow
Linux username regexes. Technically, in most situations, providing your email address from Rauthy should work, but there
are drawbacks and in some cases, such usernames lead to errors and not working commands. If Rauthy had chosen emails as
Linux usernames, you would also do very confusing stuff like <code>ssh my.user@example.com@host</code>, which is not only hard to
read, but does not even work on some systems.</p>
<p>Users are not allowed to choose their Linux username themselves. This must be done by a Rauthy admin to avoid users
trying things like <code>root</code>, <code>postgres</code> or <code>systemd</code>, which would be very bad of course. An admin needs to configure
access to hosts anyway, so there is no real drawback, that users cannot choose on their own. Maintaining a blacklist
with all possibly problematic usernames across all distros and systems is actually impossible, as Rauthy cannot even
know which names might already be special in some companies and situations.</p>
<p>As soon as an Admin has configured a Linux user for a "real" user, this user can log in to the account dashboard and a
PAM section will pop up in the tab bar. Here you can generate the auto-expiring Remote PAM Passwords that are used for
basically all actions via SSH like logging in and <code>sudo</code>, and you will see a list of all hosts you have access to. For
these PAM passwords, you have 2 static config varibales:</p>
<pre><code class="language-toml">[pam]
# The length of newly generated PAM remote passwords via the
# account dashboard. The default is fine as long as you can copy
# &amp; paste them. You may want to reduce the length here if you e.g.
# occasionally generate them on mobile and need to type them
# manually into some terminal.
#
# default: 24
# overwritten by: PAM_REMOTE_PASSWORD_LEN
remote_password_len = 24

# The TTL for newly generated PAM remote passwords in seconds.
# The default gives you plenty of time to open a few sessions in
# some terminals and maybe switch to `root` on some remote machines,
# while still expiring quick enough to be secure.
#
# default: 120
# overwritten by: PAM_REMOTE_PASSWORD_TTL
remote_password_ttl = 120
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="pam--nss-modules"><a class="header" href="#pam--nss-modules">PAM + NSS Modules</a></h1>
<p>This is a documentation for the separately managed project <a href="https://github.com/sebadob/rauthy-pam-nss">rauthy-pam-nss</a>.
It's a separate project to avoid licensing issues because of some dependencies the PAM and NSS bindings rely on.</p>
<p>These modules are basically the "client part" for all the PAM configuration you did on Rauthys side. You need to install
3 things:</p>
<ul>
<li>NSS module</li>
<li>NSS Proxy</li>
<li>PAM module</li>
</ul>
<p>The NSS module + proxy are there to dynamically resolve users, groups and hosts via Rauthy without the need to add them
to any of the local files like <code>/etc/passwd</code> or <code>/etc/group</code>. The NSS module will be called from all different sorts of
applications, that need to resolve some names. The NSS module exposes the <code>C</code> bindings and then execute the logic in
Rust. This module is pretty simple, as it only connects to a local Unix Domain Socket, where the NSS Proxy will be
listening. This proxy is the one doing the actual requests to Rauthy, as well as cache results for faster subsequent
lookups.</p>
<p>The reason why there is a proxy in between is not only to be able to cache results, but also to be more efficient and
more importantly, to only give the NSS proxy access to the actual host ID and secret, which are needed to do requests
against Rauthys API. These should have the least possible amount of exposure, because with these credentials, you could
technically find out which usernames exist on Rauthy and which do not. This is not a security issue right away, but not
leaking anything is always preferred.</p>
<p>The main configuration file for NSS lookups is <code>/etc/nsswitch.conf</code>, but no worries, it's not only pretty simple to
understand, but will also be automatically managed by the install script. The only important part about it is, if you
want to be able to use the <code>local</code> PAM groups feature, that you add <code>[SUCCESS=merge]</code> in between entries for <code>group</code>
lookups.</p>
<p>You can test very easily if NSS lookups are working with e.g. <code>getent groups</code> or <code>getent users</code>, and you should see
the Rauthy-managed resources included in the output. If at the beginning or at the end depends on your <code>nsswitch.conf</code>,
and you of course can only see users if there are any configured on Rauthy. Only if NSS lookups are working fine, you
should install the PAM module.</p>
<p>The PAM module exposes the <code>C</code> bindings for, surprise, PAM. Configuring PAM is its own, very big topic and I will not
go into detail here. There is lots of documentation and guides in the internet. For simple setups, like if you did not
do any customizations to your hosts anyway, the provided default templates should work out of the box (hopefully). If
you have done customizations, you probably know about how it's done anyway and maybe don't even want to use the provided
templates. That's up to you of course. The same for NSS, as long as you add Rauthys modules to your setup correctly, it
should work.</p>
<p>For standard scenarios, you will get an install script. It currently should upport RHEL and Debian based distros. Other
ones (suse, arch, gentoo) may need contributions to include them in the script.</p>
<h2 id="install-tldr"><a class="header" href="#install-tldr">Install TL;DR</a></h2>
<p>The PAM project is in a very early phase and there are no distro-packaged versions available. Here is the gist how to
install:</p>
<pre><code class="language-bash">curl -LO https://raw.githubusercontent.com/sebadob/rauthy-pam-nss/refs/tags/v0.1.0/install/rauthy-pam-nss-install.tar.gz \
  &amp;&amp; tar -xzf rauthy-pam-nss-install.tar.gz \
  &amp;&amp; cd rauthy-pam-nss-install
</code></pre>
<p>Then, since you should never blindly execute a random bash script from the internet, especially with <code>sudo</code>, inspect
<code>install.sh</code> and afterward install config files, NSS Proxy and (depending on availability) SELinux policy:</p>
<pre><code class="language-bash">sudo ./install.sh nss
</code></pre>
<p>Then check via e.g. <code>getent hosts</code> or <code>getent groups</code> that you get data from Rauthy. However, the script does it as well
and you should see an error message about exceeded retries it you e.g. have given invalid credentials. When NSS lookups
are working fine, proceed with the PAM module installation:</p>
<pre><code class="language-bash">sudo ./install.sh pam
</code></pre>
<p>If your OS is managed by <code>authselect</code>, you need to activate the new custom profile afterward with
<code>authselect select custom/rauthy</code>, just like mentioned in the output. On other OSes like Debian, the script will create
backups of config files and then copy the Rauthy configs in place directly.</p>
<p><strong>CAUTION:</strong> Make sure to test all the logins and things that should work at this point BEFORE logging out. Keep a
backup session open, just in case something broke. Incorrectly configured PAM modules can lock you out of your machine!</p>
<div id="admonition-danger" class="admonition admonish-danger" role="note" aria-labelledby="admonition-danger-title">
<div class="admonition-title">
<div id="admonition-danger-title">
<p>Danger</p>
</div>
<a class="admonition-anchor-link" href="work/pam_nss_modules.html#admonition-danger"></a>
</div>
<div>
<p>Before you install anything, make sure that you have at least a backup session / terminal somewhere in the background
that you don't use for installation / testing. If you mess up your PAM setup, you can easily lock yourself out of your
machine and it could be very hard, or sometimes impossible, to recover it. Keep the backup session open and don't do
anything with it, until you verified after the installation that everything is working as expected. If this should not
be the case, you should revert your PAM config. The install script will copy the old files and appends <code>.bak-non-rauthy</code>
to them.</p>
<p>If you are not experienced with NSS / PAM yet, you should not try it on any important or production machine. The best
thing would be to spin up an empty VM for testing, or at least create a new snapshot before you start. The default PAM
configs are safe, but if you do anything custom, it is very easy to make your whole system insecure if done incorrectly.</p>
</div>
</div>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="work/pam_nss_modules.html#admonition-hint"></a>
</div>
<div>
<p>The install script detects <code>authselect</code>-managed environments automatically and creates a <code>custom/rauthy</code> profile in this
case, which you need to activate manually. However, it does NOT work with other management systems yet and if it does
not find <code>authselect</code>, it will create backup copies of <code>/etc/nsswitch.conf</code> and the ones in <code>/etc/pam.d/</code> before copying
its own files in place. If you manage PAM via other systems, feel welcome to provide a PR to include it in the script.</p>
</div>
</div>
<h2 id="where-to-find-everything"><a class="header" href="#where-to-find-everything">Where to find everything</a></h2>
<p>The install script will take care of everything to get it up and running for standard scenarios. However, if you need to
debug, do custom configuration, or anything else, here is some more information:</p>
<ul>
<li>The config file can be found here: <code>/etc/rauthy/rauthy-pam-nss.toml</code></li>
<li><code>/etc/skel_rauthy</code> contains the template for home dir creation.</li>
<li>The PAM and NSS modules will go "somewhere" into <code>/lib/</code>, depending on your distro.</li>
<li>The NSS socket, which should be carefully protected, so a normal user cannot spoof it, can be found under
<code>/run/rauthy/rauthy_proxy.sock</code>. This is wrapped inside a sub-dir on purpose for 2 reasons: Additional protection via
access rights, because it needs to be world-accessible for a lot of services, and to be able to mount the dir into
a container, just in case someone maybe wants to run the NSS proxy as a quadlet. You cannot mount a socket file into
a container directly (as listener at least) and need to mount the parent directory instead.</li>
<li><code>/var/lib/pam_rauthy/</code> contains the optional session start and close scripts (enabled via the config above) and all
the PAM tokens for users that are created during login.</li>
<li>By default, the NSS proxy logs into <code>/var/run/rauthy/</code>.</li>
</ul>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="work/pam_nss_modules.html#admonition-caution"></a>
</div>
<div>
<p>It is VERY important that you never mess up the access rights for all of these files and dirs. Most importantly for the
UDS so it can never be spoofed. Everything should run as <code>root</code> for the highest level of protection. In case of the
session open and close scripts, their access rights will be checked by the PAM module before each execution and if they
don't match <code>0700</code> or are not owned by <code>root</code>, they will not be executed.</p>
</div>
</div>
<div id="admonition-caution-1" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-1-title">
<div class="admonition-title">
<div id="admonition-caution-1-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="work/pam_nss_modules.html#admonition-caution-1"></a>
</div>
<div>
<p>If you change any of the paths like e.g. the <code>data_dir</code> and you have SELinux running, you must make sure that the
correct types are added via the file context.</p>
</div>
</div>
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
<div class="admonition-title">
<div id="admonition-note-title">
<p>Note</p>
</div>
<a class="admonition-anchor-link" href="work/pam_nss_modules.html#admonition-note"></a>
</div>
<div>
<p>All of these locations and executables have dedicated SELinux types and a policy to provide additional protection. This
will of course only work on systems where SELinux is available. An AppArmor equivalent does NOT exist yet, but the
generic Linux access rules provide proper protection, at least as long a not another <code>root</code> application is malicious.</p>
</div>
</div>
<h2 id="selinux"><a class="header" href="#selinux">SELinux</a></h2>
<p>An SELinux policy with quite a lot of apps and services is already included and will be installed automatically, if
SELinux exists on your system. However, I am pretty sure that depending on the type of apps you have installed, you
might get warnings for apps about access the <code>rauthy_var_run_t</code> with <code>write</code> or <code>connectto</code>. This is generally fine to
allow for anything, as long as you don't allow anything else. <code>write</code> and <code>connectto</code> are necessary to do NSS lookups
via the NSS module.</p>
<p>If you stumble about warnings and missing types that should be added to the policy, please open an issue or directly a
PR about it. I can of course only validate for types that I come across on a daily basis. Apart from writing and
connecting to the UDS, you should not see any warnings (hopefully), but if you do, again, please open an issue / PR.</p>
<h2 id="apparmor"><a class="header" href="#apparmor">AppArmor</a></h2>
<p>I am working with RHEL based distros and SELinux exclusively and therefore don't use AppArmor and I have no experience
with it. No rules have been added to the repo or install script. If you use AppArmor, please feel free to provide a PR.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="reference-config"><a class="header" href="#reference-config">Reference Config</a></h1>
<p>This shows a full example config with (hopefully) every value nicely described.</p>
<p>You can configure a lot here, but the most important variables you most-likely want to change when going into production
are the following. Lines beginning with <code>!!!</code> are absolutely mandatory. The order matches their location in the
reference config below.</p>
<ul>
<li><code>user_registration.enable</code>, <code>user_registration.domain_restriction</code></li>
<li><code>access.peer_ip_header_name</code> - when behind a CDN</li>
<li>the whole <code>[cluster]</code> + <code>[database]</code> + <code>[email]</code> sections</li>
<li><code>bootstrap.admin_email</code></li>
<li>!!! <code>[encryption]</code></li>
<li><code>hashing.max_hash_threads</code> in combination with <code>server.http_workers</code></li>
<li>!!! <code>server.pub_url</code> + <code>server.proxy_mode</code> + <code>server.trusted_proxies</code></li>
<li><code>[tls]</code> if you don't terminate TLS on your reverse proxy</li>
<li>!!! <code>webauthn.rp_id</code> + <code>webauthn.rp_origin</code> + <code>webauthn.rp_name</code></li>
</ul>
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
<div class="admonition-title">
<div id="admonition-caution-title">
<p>Caution</p>
</div>
<a class="admonition-anchor-link" href="config/config.html#admonition-caution"></a>
</div>
<div>
<p>When you go into production, make sure that you provide the included secrets / sensistive information in this
file in an appropriate way. With docker, you can leave them inside this file (with proper access rights!), but when
deploying with Kubernetes, either extract these values into Kubernetes secrets, or simply provide the whole config as
one secret (my preferred approach).</p>
</div>
</div>
<pre><code class="language-toml">[access]
# If set to true, the `/userinfo` endpoint will do additional
# validations. The non-strict mode will fetch the user by id from
# the `sub` claim and make sure it still exists and is enabled. The
# strict validation will do additional database fetches and
# validates every possible value. Additionally, it will look up a
# possibly linked user device from the `did` claim and make sure it
# still exists. It will also extract the `client_id` the token has
# been originally issued for from the `azp` claim, fetch it and
# make sure it still exists and is enabled.
#
# If you need CORS headers on the /userinfo endpoint, you must
# enable strict mode, because otherwise the additional data would
# be missing.
#
# If you don't need the extra validations, you can set this to
# `false` to save some resources, if your clients to a lot of
# `/userinfo` lookups.
#
# default: true
# overwritten by: USERINFO_STRICT
#userinfo_strict = true

# Can be set to `true` to disable authorization on `/oidc/introspect.
# This should usually never be done, but since the auth on that
# endpoint is not really standardized, you may run into issues with
# your client app. If so, please open an issue about it.
#
# default: false
# overwritten by: DANGER_DISABLE_INTROSPECT_AUTH
#danger_disable_introspect_auth = false

# By default, `refresh_token`s will have an `nbf` claim, making them
# valid at `access_token_lifetime - 60 seconds`. Any usage before
# this time will result in invalidation of not only the token itself,
# but also all other linked sessions and tokens for this user to
# prevent damage in case a client leaked the token by accident.
# However, there are bad / lazy client implementations that do not
# respect either `nbf` in the `refresh_token`, or the `exp` claim in
# `access_token` and will refresh early while the current
# access_token is still valid. This does not only waste resources
# and time, but also makes it possible to have multiple valid
# `access_token`s at the same time for the same session. You should
# only disable the `nbf` claim if you have a good reason to do so.
# If disabled, the `nbf` claim will still exist, but always set to
# *now*.
#
# default: false
# overwritten by: DISABLE_REFRESH_TOKEN_NBF
#disable_refresh_token_nbf = false

# If set to true, a violation inside the CSRF protection middleware
# based on Sec-* headers will block invalid requests. Usually you
# always want this enabled. You may only set it to false during the
# first testing phase if you experience any issues with an already
# existing Rauthy deployment. In future releases, it will not be
# possible to disable these blocks.
#
# default: true
# overwritten by: SEC_HEADER_BLOCK
#sec_header_block = true

# If set to 'true', this will validate the remote peer IP address with
# each request and compare it with the IP which was used during the
# initial session creation / login. If the IP is different, the session
# will be rejected.
#
# This is a security hardening and prevents stolen access credentials,
# for instance if an attacker might have copied the encrypted session
# cookie and the XSRF token from the local storage from a user.
# However, this event is really unlikely, since it may only happen if
# an attacker has direct access to the machine itself.
#
# If your users are using mobile networks and get new IP addresses all
# the time, this means they have to do a new login each time. This is
# no big deal at all with Webauthn / FIDO keys anyway and should not
# be a reason to deactivate this feature.
#
# CAUTION: If you are running behind a reverse proxy which does not
# provide the X-FORWARDED-FOR header correctly, or you have the
# `proxy_mode` in this config disabled, this feature will not work.
# You can validate the IPs for each session in the Admin UI. If these
# are correct, your setup is okay.
#
# default: true
# overwritten by: SESSION_VALIDATE_IP
#session_validate_ip = true

# By default, Rauthy will log a warning into the logs, if an active
# password reset form is being access multiple times from different
# hosts. You can set this to `true` to actually block any following
# request after the initial one. This hardens the security of the
# password reset form a bit more, but will create problems with
# E-Mail providers like Microsoft, which cans the customers E-Mails
# and even uses links inside, which make them unusable with this set
# to `true`.
#
# This feature works by setting an encrypted cookie to the host
# whichever opens the password reset form for the very first time.
# All subsequent requests either need to provide that cookie or would
# otherwise be rejected.
#
# default: false
# overwritten by: PASSWORD_RESET_COOKIE_BINDING
#password_reset_cookie_binding = false

# Can be set to extract the remote client peer IP from a custom
# header name instead of the default mechanisms. This is needed
# when you are running behind a proxy which does not set the
# `X-REAL-IP` or `X-FORWARDED-FOR` headers correctly, or for
# instance when you proxy your requests through a CDN like
# Cloudflare, which adds custom headers in this case. For instance,
# if your requests are proxied through cloudflare, your would set
# `CF-Connecting-IP`.
#
# overwritten by: PEER_IP_HEADER_NAME
#peer_ip_header_name = ''

# You can set different security levels for Rauthy's cookies. The
# safest option would be 'host', but may not be desirable when you
# host an application on the same origin behind a reverse proxy.
# In this case you might want to restrict to 'secure', which will
# then take the `cookie_set_path` from below into account. The last
# option is 'danger-insecure' which really should never be used
# unless you are just testing on localhost.
#
# default: host
# overwritten by: COOKIE_MODE
#cookie_mode = 'host'

# If set to 'true', Rauthy will bind the cookie to the `/auth`
# path. You may want to change this only for very specific reasons
# and if you are in such a situation, where you need this, you will
# know it. Otherwise, don't change this value.
#
# default: true
# overwritten by: COOKIE_SET_PATH
#cookie_set_path = true

# Sets the limit in characters for the maximum JWT token length that
# will be accepted when validating it. The default of 4096 is high
# enough that you should never worry about this value. A typical
# `id_token` with quite a few additional custom attributes and scopes,
# signed with RS512, will usually be below 2000 characters.
#
# Only if you create very big tokens and you get errors on the
# `/userinfo` for instance, you might want to increase this value.
# Otherwise, don't worry about it.
#
# default: 4096
# overwritten by: TOKEN_LEN_LIMIT
#token_len_limit = 4096

# If set to `true`, the `/auth/v1/whoami` endpoint will return all
# request headers. Only the `Cookie` and Rauthys internal CSRF headers
# will be hidden. Since this has the potential to leak sensitive
# information, depending on you networking, this is disabled by default.
#
# default: false
# overwritten by: WHOAMI_HEADERS
#whoami_headers = false

[atproto]
# Set to `true` to enable the ATProto provider. If the public URL is
# 'localhost' it should be changed to '127.0.0.1', if `dev_mode = true`
# this also applies for the `provider_callback_url`.
#
# default: false
# overwritten by: ATPROTO_ENABLE
#enable = false

[auth_headers]
# You can enable authn/authz headers which would be added to the
# response of the `/auth/v1/oidc/forward_auth` endpoint. When set to
# `true`, the headers below will be added to authenticated requests.
# These could be used on legacy downstream applications, that don't
# support OIDC on their own.
#
# However, be careful when using this, since this kind of authn/authz
# has a lot of pitfalls out of the scope of Rauthy.
#
# default: false
# overwritten by: AUTH_HEADERS_ENABLE
#enable = true

# Configure the header names being used for the different values. You
# can change them to your needs, if you cannot easily change your
# downstream apps.
#
# default: x-forwarded-user
# overwritten by: AUTH_HEADER_USER
#user = 'x-forwarded-user'
# default: x-forwarded-user-roles
# overwritten by: AUTH_HEADER_ROLES
#roles = 'x-forwarded-user-roles'
# default: x-forwarded-user-groups
# overwritten by: AUTH_HEADER_GROUPS
#groups = 'x-forwarded-user-groups'
# default: x-forwarded-user-email
# overwritten by: AUTH_HEADER_EMAIL
#email = 'x-forwarded-user-email'
# default: x-forwarded-user-email-verified
# overwritten by: AUTH_HEADER_EMAIL_VERIFIED
#email_verified = 'x-forwarded-user-email-verified'
# default: x-forwarded-user-family-name
# overwritten by: AUTH_HEADER_FAMILY_NAME
#family_name = 'x-forwarded-user-family-name'
# default: x-forwarded-user-given-name
# overwritten by: AUTH_HEADER_GIVEN_NAME
#given_name = 'x-forwarded-user-given-name'
# default: x-forwarded-user-mfa
# overwritten by: AUTH_HEADER_MFA
#mfa = 'x-forwarded-user-mfa'

[backchannel_logout]
# The maximum amount of retries made for a failed backchannel logout.
# Failed backchannel logouts will be retried every 60 - 90 seconds
# from all cluster nodes. The timeout between retries is randomized
# to avoid overloading clients. It will be executed on each cluster
# member to increase the chance of a successful logout in case of
# network segmentations.
#
# default: 100
# overwritten by: BACKCHANNEL_LOGOUT_RETRY_COUNT
#retry_count = 100

# The lifetime / validity for Logout Tokens issued by Rauthy in
# seconds. These Logout Tokens are being generated during OIDC
# Backchannel Logout requests to configured clients. The token
# lifetime should be as short as possible and at most 120 seconds.
#
# default: 30
# overwritten by: LOGOUT_TOKEN_LIFETIME
#token_lifetime = 30

# You can allow a clock skew during the validation of Logout Tokens,
# when Rauthy is being used as a client for an upstream auth
# provider that uses backchannel logout.
#
# The allowed skew will be in seconds and a value of e.g. 5 would
# mean, that 5 seconds are added to the `iat` and `exp` claim
# validations and expand the range.
#
# default: 5
# overwritten by: LOGOUT_TOKEN_ALLOW_CLOCK_SKEW
#allow_clock_skew = 5

# The maximum allowed lifetime for Logout Tokens. This value is
# a security check for upstream auth providers. If Rauthy
# receives a Logout Token, it will check and validate, that the
# difference between `iat` and `exp` is not greater than
# `allowed_token_lifetime`. This means Rauthy will reject Logout
# Tokens from clients with a way too long validity and therefore
# poor implementations. The RFC states that tokens should be
# valid for at most 120 seconds.
#
# default: 120
# overwritten by: LOGOUT_TOKEN_ALLOWED_LIFETIME
#allowed_token_lifetime = 120

[bootstrap]
# If set, the email of the default admin will be changed during
# the initialization of an empty production database.
#
# default: 'admin@localhost'
# overwritten by: BOOTSTRAP_ADMIN_EMAIL
#admin_email = 'admin@localhost'

# If set, this plain text password will be used for the initial
# admin password instead of generating a random password.
#
# default: random -&gt; see logs on first start
# overwritten by: BOOTSTRAP_ADMIN_PASSWORD_PLAIN
#password_plain = '123SuperSafe'

# If set, this will take the Argon2ID hashed password during the
# initialization of an empty production database. If both
# `password_plain` and `pasword_argon2id` are set, the hashed
# version will always be prioritized.
#
# default: random -&gt; see logs on first start
# overwritten by: BOOTSTRAP_ADMIN_PASSWORD_ARGON2ID
#pasword_argon2id = '$argon2id$v=19$m=32768,t=3,p=2$mK+3taI5mnA+Gx8OjjKn5Q$XsOmyvt9fr0V7Dghhv3D0aTe/FjF36BfNS5QlxOPep0'

# You can provide an API Key during the initial prod database
# bootstrap. This key must match the format and pass validation.
# You need to provide it as a base64 encoded JSON in the format:
#
# ```
# struct ApiKeyRequest {
#     /// Validation: `^[a-zA-Z0-9_-/]{2,24}$`
#     name: String,
#     /// Unix timestamp in seconds in the future (max year 2099)
#     exp: Option&lt;i64&gt;,
#     access: Vec&lt;ApiKeyAccess&gt;,
# }
#
# struct ApiKeyAccess {
#     group: AccessGroup,
#     access_rights: Vec&lt;AccessRights&gt;,
# }
#
# enum AccessGroup {
#     Blacklist,
#     Clients,
#     Events,
#     Generic,
#     Groups,
#     Roles,
#     Secrets,
#     Sessions,
#     Scopes,
#     UserAttributes,
#     Users,
# }
#
# #[serde(rename_all="lowercase")]
# enum AccessRights {
#     Read,
#     Create,
#     Update,
#     Delete,
# }
# ```
#
# You can use the `api_key_example.json` from `/` as
# an example. Afterward, just
# `base64 api_key_example.json | tr -d '\n'`
#
# overwritten by: BOOTSTRAP_API_KEY
#api_key = 'ewogICJuYW1lIjogImJvb3RzdHJhcCIsCiAgImV4cCI6IDE3MzU1OTk2MDAsCiAgImFjY2VzcyI6IFsKICAgIHsKICAgICAgImdyb3VwIjogIkNsaWVudHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6IFsKICAgICAgICAicmVhZCIsCiAgICAgICAgImNyZWF0ZSIsCiAgICAgICAgInVwZGF0ZSIsCiAgICAgICAgImRlbGV0ZSIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImdyb3VwIjogIlJvbGVzIiwKICAgICAgImFjY2Vzc19yaWdodHMiOiBbCiAgICAgICAgInJlYWQiLAogICAgICAgICJjcmVhdGUiLAogICAgICAgICJ1cGRhdGUiLAogICAgICAgICJkZWxldGUiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJncm91cCI6ICJHcm91cHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6IFsKICAgICAgICAicmVhZCIsCiAgICAgICAgImNyZWF0ZSIsCiAgICAgICAgInVwZGF0ZSIsCiAgICAgICAgImRlbGV0ZSIKICAgICAgXQogICAgfQogIF0KfQ=='

# The secret for the above defined bootstrap API Key.
# This must be at least 64 alphanumeric characters long.
# You will be able to use that key afterward with setting
# the `Authorization` header:
#
# `Authorization: API-Key &lt;your_key_name_from_above&gt;$&lt;this_secret&gt;`
#
# overwritten by: BOOTSTRAP_API_KEY_SECRET
#api_key_secret = 'twUA2M7RZ8H3FyJHbti2AcMADPDCxDqUKbvi8FDnm3nYidwQx57Wfv6iaVTQynMh'

[cluster]
# Can be set to 'k8s' to try to split off the node id from the hostname
# when Hiqlite is running as a StatefulSet inside Kubernetes.
#
# default: unset
# overwritten by: HQL_NODE_ID_FROM
#node_id_from = "k8s"

# The node id must exist in the nodes and there must always be
# at least a node with ID 1
# Will be ignored if `node_id_from = k8s`
#
# At least `node_id_from` or `node_id` are required.
#
# default: 0 (invalid)
# overwritten by: HQL_NODE_ID
node_id = 1

# All cluster member nodes.
# Each array value must have the following format:
# `id addr_raft addr_api`
#
# default: ["1 localhost:8100 localhost:8200"]
# overwritten by: HQL_NODES
nodes = ["1 localhost:8100 localhost:8200"]

# You can set the listen addresses for both the API and Raft servers.
# These need to somewaht match the definition for the `nodes` above,
# with the difference, that a `node` address can be resolved via DNS,
# while the listen addresses must be IP addresses.
#
# The default for both of these is "0.0.0.0" which makes them listen
# on all interfaces.
# overwritten by: HQL_LISTEN_ADDR_API
#listen_addr_api = "0.0.0.0"
# overwritten by: HQL_LISTEN_ADDR_RAFT
#listen_addr_raft = "0.0.0.0"

# The data dir hiqlite will store raft logs and state machine data in.
#
# default: data
# overwritten by: HQL_DATA_DIR
#data_dir = "data"

# The file name of the SQLite database in the state machine folder.
#
# default: hiqlite.db
# overwritten by: HQL_FILENAME_DB
#filename_db = "hiqlite.db"

# If set to `true`, all SQL statements will be logged for debugging
# purposes.
#
# default: false
# overwritten by: HQL_LOG_STATEMENTS
#log_statements = false

# The size of the internal cache for prepared statements.
#
# default: 1000
#prepared_statement_cache_capacity = 1000

# The size of the pooled connections for local database reads.
#
# Do not confuse this with a pool size for network databases, as it
# is much more efficient. You can't really translate between them,
# because it depends on many things, but assuming a factor of 10 is
# a good start. This means, if you needed a (read) pool size of 40
# connections for something like a postgres before, you should start
# at a `read_pool_size` of 4.
#
# Keep in mind that this pool is only used for reads and writes will
# travel through the Raft and have their own dedicated connection.
#
# default: 4
# overwritten by: HQL_READ_POOL_SIZE
#read_pool_size = 4

# Setting for Raft Log syncing / flushing.
#
# This value is probably the most important, depending on your needs.
# It has a huge impact on both performance and consistency.
#
# You can set 3 different levels:
# - immediate
# - immediate_async
# - interval_&lt;time_ms&gt; (e.g. interval_200)
#
# If you run a single instance "Cluster", you most probably want
# `immediate` to have the highest degree of consistency. If set
# to `immediate`, the Raft will block until data has been flushed
# to disk. This is especially important for a single instance
# deployment, because there is no way to recover state from other
# nodes or re-sync a maybe corrupted WAL file.
# `immediate` has a very huge negative impact on throughput, and
# it puts a lot of stress on your disk, which is important to
# consider for SSDs.
#
# `immediate_async` puts the same amount of stress on your SSD
# and flushed all buffers to disk immediately after a single
# Raft Log was saved. Unlike `immediate` however, it does not
# wait for completion and directly returns `success` to the
# Raft Engine. You have a bit less consistency guarantees in
# exchange for basically the same throughput as with `interval`
# syncing.
#
# The `interval_&lt;ms&gt;` option will not flush immediately. Flushes
# will be triggered by an external ticker task top flush buffers
# every &lt;ms&gt; ms, and then only if buffers are dirty, meaning if
# any data has been updated since the last sync. This setting
# has the highest throughput and puts the lowest amount of stress
# on your SSD.
# CAUTION: You probably never want to use this setting for a
# single instance, since it may be the case that if you are
# unlucky and your app crashes before buffers are synced, that
# your WAL file might end up being corrupted. Even though very
# unlikely in real world (can be force-produced though), you
# would need to re-sync from a healthy cluster member in such
# a case, if the automactic WAL repair does not succeed.
#
# default: immediate_async
# overwritten by: HQL_LOG_SYNC
#log_sync = "immediate_async"

# Hiqlite WAL files (when not using the `rocksdb` feature) will
# always have a fixed size, even when they are still "empty", to
# reduce disk operations while writing. You can set the WAL size
# in bytes. The default value is 2 MB, while the minimum size is
# 8 kB.
#
# default: 2097152
# overwritten by: HQL_WAL_SIZE
#wal_size = 2097152

# Set to `false` to store Cache WAL files + Snapshots in-memory only.
# If you run a Cluster, a Node can re-sync cache data after a restart.
# However, if you restart too quickly or shut down the whole cluster,
# all your cached data will be gone.
# In-memory only hugegly increases the throughput though, so it
# depends on your needs, what you should prefer.
#
# default: true
# overwritten by: HQL_CACHE_STORAGE_DISK
#cache_storage_disk = true

# Sets the limit when the Raft will trigger the creation of a new
# state machine snapshot and purge all logs that are included in
# the snapshot.
# Higher values can achieve more throughput in very write heavy
# situations but will end up in more disk usage and longer
# snapshot creations / log purges.
#
# default: 10000
# overwritten by: HQL_LOGS_UNTIL_SNAPSHOT
#logs_until_snapshot = 10000

# The artificial shutdown delay to add in multi-node environments.
# This value is being added before finally shutting down a node
# to make sure rolling releases can be executed graceful with
# proper leader switches. You may want to increase this value if
# you are in an environment with huge in-memory caches and you
# want to provide a bit more headroom for the snapshot replication
# after restarts.
#
# default: 5000
# overwritten by: HQL_SHUTDOWN_DELAY_MILLS
#shutdown_delay_millis = 5000

# If given, these keys / certificates will be used to establish
# TLS connections between nodes.
#
# values are optional, overwritten by: HQL_TLS_{RAFT|API}_{KEY|CERT}
# overwritten by: HQL_TLS_RAFT_KEY
#tls_raft_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_CERT
#tls_raft_cert = "tls/tls.crt"
#tls_raft_danger_tls_no_verify = true

# overwritten by: HQL_TLS_API_KEY
#tls_api_key = "tls/tls.key"
# overwritten by: HQL_TLS_RAFT_KEY
#tls_api_cert = "tls/tls.crt"
#tls_api_danger_tls_no_verify = true

# Secrets for Raft internal authentication as well as for the API.
# These must be at least 16 characters long and you should provide
# different ones for both variables.
#
# default: not set - required
# overwritten by: HQL_SECRET_RAFT
secret_raft = "SuperSecureSecret1337"
# default: not set - required
# overwritten by: HQL_SECRET_API
secret_api = "SuperSecureSecret1337"

# Configures the initial delay in seconds that should be applied
# to `&lt;API&gt;/health` checks. During the first X seconds after node
# start, health checks will always return true to solve a chicken-
# and-egg problem when you want to cold-start a cluster while
# relying on `readinessProbe` checks.
#
# default: 30
#health_check_delay_secs = 30

# When the auto-backup task should run.
# Accepts cron syntax:
# "sec min hour day_of_month month day_of_week year"
#
# default: "0 30 2 * * * *"
# overwritten by: HQL_BACKUP_CRON
#backup_cron = "0 30 2 * * * *"

# Backups older than the configured days will be cleaned up on S3
# after the backup cron job `backup_cron`.
#
# default: 30
# overwritten by: HQL_BACKUP_KEEP_DAYS
#backup_keep_days = 30

# Backups older than the configured days will be cleaned up locally
# after each `Client::backup()` and the cron job `HQL_BACKUP_CRON`.
#
# default: 3
# overwritten by: HQL_BACKUP_KEEP_DAYS_LOCAL
#backup_keep_days_local = 3

# If you ever need to restore from a backup, the process is simple.
# 1. Have the cluster shut down. This is probably the case anyway, if
#    you need to restore from a backup.
# 2. Provide a backup file name on S3 storage with the
#    `HQL_BACKUP_RESTORE` value with prefix `s3:` (encrypted), or a file
#    on disk (plain sqlite file) with the prefix `file:`.
# 3. Start up the cluster again.
# 4. After the restart, make sure to remove the HQL_BACKUP_RESTORE
#    env value.
#
# CAUTION: can only be set via ENV VAR temporarily
#HQL_BACKUP_RESTORE=

# The Hiqlite backup restore process checks the `_metadata` table
# in backups as an integrity check. If you however want to "restore"
# from an already existing default SQLite file, you can disable
# this validation to make the restore process succeed anyway.
# To do so, set `HQL_BACKUP_SKIP_VALIDATION=true`.
#
# CAUTION: can only be set via ENV VAR temporarily
#HQL_BACKUP_SKIP_VALIDATION=

# Access values for the S3 bucket where backups will be pushed to.
# overwritten by: HQL_S3_URL
#s3_url = "https://s3.example.com"
# overwritten by: HQL_S3_BUCKET
#s3_bucket = "my_bucket"
# overwritten by: HQL_S3_REGION
#s3_region = "my_region"
# overwritten by: HQL_S3_PATH_STYLE
#s3_path_style = true
# overwritten by: HQL_S3_KEY
#s3_key = "my_key"
# overwritten by: HQL_S3_SECRET
#s3_secret = "my_secret"

# The password for the dashboard as b64 encoded Argon2ID hash.
# If left empty, the Dashboard will not be exposed.
#
# '123SuperMegaSafe' in this example
# overwritten by: HQL_PASSWORD_DASHBOARD
#password_dashboard = "JGFyZ29uMmlkJHY9MTkkbT0zMix0PTIscD0xJE9FbFZURnAwU0V0bFJ6ZFBlSEZDT0EkTklCN0txTy8vanB4WFE5bUdCaVM2SlhraEpwaWVYOFRUNW5qdG9wcXkzQQ=="

# Can be set to `true` during local dev and testing to issue
# insecure cookies
#
# default: false
# overwritten by: HQL_INSECURE_COOKIE
#insecure_cookie = false

# You can reset the Raft Logs + Metadata when set to
# `true`. This can be helpful, if you e.g. run a single
# instance "cluster" and encountered an unrecoverable
# error at startup, like e.g. a corrupted Raft Logs volume.
# If you have an HA cluster though, in such a situation,
# a volume cleanup for the broken node and clean cluster
# re-join + sync is always preferred and the safer option.
#
# Another situation where this option can be used, if you
# want to trigger a Log ID roll-over and for some reason,
# you either cannot or do not want to apply a backup to
# achieve this. This may only be necessary if your Log ID
# almost reached `18446744073709551615`, which in reality
# will probably never happen in less than 10 years.
# Applying a backup at that point is still the safer
# option, because it is not possible to make a mistake,
# as long as you wait for it to finish.
#
# Be VERY CAREFUL with this option! If used incorrectly,
# you can destroy a Raft cluster and end up with an
# inconsistent state, so your only chance last chance
# is to either re-create the whole cluster, or apply a
# backup.
#
# This option will leave the state machine in place,
# but delete Raft WAL, Metadata and Snapshots!
# Local backups will be left in place.
#
# This option should be seen as a last resort.
# USE WITH CARE!
#
# !!!
# This value only exists here for completeness.
# You can only set it via ENV to be able to remove it
# immediately again after a start.
# !!!
#
# default: false
#HQL_DANGER_RAFT_STATE_RESET=true

[database]
# Hiqlite is the default database for Rauthy.
# You can opt-out and use Postgres instead by setting this
# value to `false`. Rauthy will then read the `pg_*` values
# below to establish a Postgres connection.
#
# default: true
# overwritten by: HIQLITE
hiqlite = true

# Defines the time in seconds after which the `/health` endpoint
# includes HA quorum checks. The initial delay solves problems
# like Kubernetes StatefulSet starts that include the health
# endpoint in the scheduling routine. In these cases, the scheduler
# will not start other Pods if the first does not become healthy.
#
# This is a chicken-and-egg problem which the delay solves.
# There is usually no need to adjust this value.
#
# default: 30
# overwritten by: HEALTH_CHECK_DELAY_SECS
#health_check_delay_secs = 30

# If you set `hiqlite = false` and want to use Postgres as your
# database, you need to set the following variables.
# These will be ignored as long as `hiqlite = true`.
#
# overwritten by: PG_HOST
pg_host = 'localhost'
# default: 5432
# overwritten by: PG_PORT
#pg_port = 5432
# overwritten by: PG_USER
pg_user = 'rauthy'
# overwritten by: PG_PASSWORD
pg_password = '123SuperSafe'
# default: rauthy
# overwritten by: PG_DB_NAME
#pg_db_name = 'rauthy'

# If your database uses a self-signed certificate, which cannot
# be verified, you might want to set this to `true`.
#
# default: false
# overwritten by: PG_TLS_NO_VERIFY
#pg_tls_no_verify = false

# Max DB connections for the Postgres pool.
#
# default: 20
# overwritten by: PG_MAX_CONN
#pg_max_conn = 20

# If specified, the currently configured Database will be
# DELETED and OVERWRITTEN with a migration from the given
# database with this variable. Can be used to migrate between
# different databases.
# To migrate from Hiqlite, use the `sqlite:path/to/database.sqlite`
# format. To migrate from postgres, just set `postgres` and then
# all the
# `MIGRATE_PG_*` values below.
#
# !!! USE WITH CARE !!!
#
# Cannot be set in this config, MUST be an ENV var!
# Exists here only for completeness.
#MIGRATE_DB_FROM = sqlite:data/state_machine/db/hiqlite.db
#MIGRATE_DB_FROM = postgresql

# If `migrate_db_from = postgres`, these values are mandatory to
# open the database connection to the Postgres database you want
# to migrate away from.
# overwritten by: MIGRATE_PG_HOST
#migrate_pg_host = 'localhost'
# default: 5432
# overwritten by: MIGRATE_PG_PORT
#migrate_pg_port = 5432
# overwritten by: MIGRATE_PG_USER
#migrate_pg_user = 'rauthy'
# overwritten by: MIGRATE_PG_PASSWORD
#migrate_pg_password = '123SuperSafe'
# default: rauthy
# overwritten by: MIGRATE_PG_DB_NAME
#migrate_pg_db_name = 'rauthy'

# The interval in minutes in which the scheduler for expired
# users should run. If this finds expired users, it invalidates
# all existing sessions and refresh tokens for this user.
#
# default: 60
# overwritten by: SCHED_USER_EXP_MINS
#sched_user_exp_mins = 60

# The threshold in minutes after which time the user expiry
# scheduler should automatically clean up expired users. If not
# set at all, expired users will never be cleaned up automatically.
#
# default: disabled / not set
# overwritten by: SCHED_USER_EXP_DELETE_MINS
#sched_user_exp_delete_mins = 7200

[device_grant]
# The lifetime in secods  of auth codes for the Device Authorization
# Grant flow. You may increase the default of 300 seconds, if you have
# "slow users" and they are simply not fast enough with the verification.
#
# default: 300
# overwritten by: DEVICE_GRANT_CODE_LIFETIME
#code_lifetime = 300

# The length of the `user_code` the user has to enter manually for
# auth request validation. This must be &lt; 64 characters.
#
# default: 8
# overwritten by: DEVICE_GRANT_USER_CODE_LENGTH
#user_code_length = 8

# Specifies the rate-limit in seconds per IP for starting new Device
# Authorization Grant flows. This is especially important for public
# clients, because a code request for this flow will actually create
# cached data. If this happened on an unrestricted, open endpoint,
# the application could easily be DoS'ed.
# If you use the `device_code` grant with confidential clients only,
# you can leave this unset, which will not rate-limit the endpoint.
#
# default: not set
# overwritten by: DEVICE_GRANT_RATE_LIMIT
#rate_limit = 1

# The interval in seconds which devices are told to use when they
# poll the token endpoint during Device Authorization Grant flow.
#
# default: 5
# overwritten by: DEVICE_GRANT_POLL_INTERVAL
#poll_interval = 5

# You can define a global lifetime in hours for refresh tokens issued
# from a Device Authorization Grant flow. You might want to have a
# higher lifetime than normal refresh tokens, because they might be
# used in IoT devices which may be offline for longer periods of time.
#
# default: 72
# overwritten by: DEVICE_GRANT_REFRESH_TOKEN_LIFETIME
#refresh_token_lifetime = 72

[dpop]
# May be set to 'false' to disable forcing the usage of DPoP nonce's.
#
# default: true
# overwritten by: DPOP_FORCE_NONCE
#force_nonce = true

# Lifetime in seconds for DPoP nonces. These are used to limit the
# lifetime of a client's DPoP proof. Do not set lower than 30 seconds
# to avoid too many failed client token requests.
#
# default: 900
# overwritten by: DPOP_NONCE_EXP
#nonce_exp = 900

[dynamic_clients]
# If set to `true`, dynamic client registration will be enabled.
# Only activate this, if you really need it, and you know, what
# you are doing. The dynamic client registration without further
# restriction will allow anyone to register new clients, even
# bots and spammers, and this may create security issues, if not
# handled properly and your users just login blindly to any client
# they get redirected to.
#
# default: false
# overwritten by: ENABLE_DYN_CLIENT_REG
#enable = true

# If specified, this secret token will be expected during
# dynamic client registrations to be given as a
# `Bearer &lt;DYN_CLIENT_REG_TOKEN&gt;` token. Needs to be communicated
# in advance.
#
# default: &lt;empty&gt;
# overwritten by: DYN_CLIENT_REG_TOKEN
#reg_token = '123SuperSafeToken'

# The default token lifetime in seconds for a dynamic client,
# that will be set during the registration.
# This value can be modified manually after registration via
# the Admin UI like for any other client.
#
# default: 1800
# overwritten by: DYN_CLIENT_DEFAULT_TOKEN_LIFETIME
#default_token_lifetime = 1800

# If set to 'true', client secret and registration token will be
# automatically rotated each time a dynamic client updates itself
# via the PUT endpoint. This is the only way that secret rotation
# could be automated safely.
# However, this is not mandatory by RFC and it may lead to errors,
# if the dynamic clients are not implemented properly to check for
# and update their secrets after they have done a request.
# If you get into secret-problems with dynamic clients, you should
# update the client to check for new secrets, if this is under your
# control. If you cannot do anything about it, you might set this
# value to 'false' to disable secret rotation.
#
# default: true
# overwritten by: DYN_CLIENT_SECRET_AUTO_ROTATE
#secret_auto_rotate = true

# This scheduler will be running in the background, if
# `ENABLE_DYN_CLIENT_REG=true`. It will auto-delete dynamic clients,
# that have been registered and not been used in the following
# `DYN_CLIENT_CLEANUP_THRES` hours.
# Since a dynamic client should be used right away, this should never
# be a problem with "real" clients, that are not bots or spammers.
#
# The interval is specified in minutes.
# default: 60
# overwritten by: DYN_CLIENT_CLEANUP_INTERVAL
#cleanup_interval = 60

# The threshold for newly registered dynamic clients cleanup, if
# not being used within this timeframe. This is a helper to keep
# the database clean, if you are not using any `DYN_CLIENT_REG_TOKEN`.
# The threshold should be specified in minutes. Any client, that has
# not been used within this time after the registration will be
# automatically deleted.
#
# Note: This scheduler will only run, if you have not set any
# `DYN_CLIENT_REG_TOKEN`.
#
# default: 60
# overwritten by: DYN_CLIENT_CLEANUP_MINUTES
#cleanup_minutes = 60

# The rate-limiter timeout for dynamic client registration.
# This is the timeout in seconds which will prevent an IP from
# registering another dynamic client, if no `DYN_CLIENT_REG_TOKEN`
# is set. With a `DYN_CLIENT_REG_TOKEN`, the rate-limiter will not
# be applied.
#
# default: 60
# overwritten by: DYN_CLIENT_RATE_LIMIT_SEC
#rate_limit_sec = 60

[email]
# This contact information will be added to the `rauthy`client
# within the anti lockout rule with each new restart.
#
# overwritten by: RAUTHY_ADMIN_EMAIL
rauthy_admin_email = 'admin@localhost'

# Will be used as the prefix for the E-Mail subject for each E-Mail
# that will be sent out to a client.
# This can be used to further customize your deployment.
#
# default: "Rauthy IAM"
# overwritten by: EMAIL_SUB_PREFIX
sub_prefix = 'Rauthy IAM'

# Rauthy will force TLS and try a downgrade to STARTTLS, if
# TLS fails. It will never allow an unencrypted connection.
# You might want to set `SMTP_DANGER_INSECURE=true` if you
# need this for local dev.
#
# overwritten by: SMTP_URL
smtp_url = 'localhost'
# optional, default will be used depending on TLS / STARTTLS
# overwritten by: SMTP_PORT
#smtp_port =
# overwritten by: SMTP_USERNAME
#smtp_username =
# overwritten by: SMTP_PASSWORD
#smtp_password =
# Format: "Rauthy &lt;rauthy@localhost&gt;"
# default: "Rauthy &lt;rauthy@localhost&gt;"
# overwritten by: SMTP_FROM
#smtp_from = 'Rauthy &lt;rauthy@localhost&gt;'

# You usually do not need to change this value. The 'default'
# will connect via SMTP PLAIN or LOGIN, which works in almost
# all scenarios. However, you can change it to `xoauth2` to
# connect via XOAUTH2. In addition, there is also `microsoft_graph`.
# It is a custom implementaion that does not use SMTP anymore,
# but instead the Microsoft Azure Graph API. You should not use
# it, unless you have no other choice or a very good reason.
#
# If you specify another value than ´default`, you must provide
# all the additional `x_oauth_*` values bwloe.
#
# possible values: 'default', 'xoauth2', 'microsoft_graph'
# default: 'default'
# overwritten by: SMTP_CONN_MODE
#smtp_conn_mode = 'default'

# These values must be given if `auth_xoauth2 = true`.
# They will be used for a `client_credentials` request
# to the `xoauth_url` to retrieve a token, that then
# will be used for authentication via SMTP XOAUTH2.
#
# overwritten by: SMTP_XOAUTH2_URL
#xoauth_url = ''
# overwritten by: SMTP_XOAUTH2_CLIENT_ID
#xoauth_client_id = ''
# overwritten by: SMTP_XOAUTH2_CLIENT_SECRET
#xoauth_client_secret = ''
# overwritten by: SMTP_XOAUTH2_SCOPE
#xoauth_scope = ''

# Only needed if `smtp_conn_mode = 'microsoft_graph'`:
# https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&amp;tabs=http
#
# default: not set
# overwritten by: SMTP_MICROSOFT_GRAPH_URI
#microsoft_graph_uri = ''

# The number of retries that should be done for connecting to
# the given SMTP_URL.
# When these retries are exceeded without success, Rauthy will
# panic and exit, so no E-Mail can get lost silently because of
# a missing SMTP connection.
#
# default: 3
# overwritten by: SMTP_CONNECT_RETRIES
#connect_retries = 3

# You can set this to `true` to allow an unencrypted and
# unauthenticated SMTP connection to an SMTP relay on your localhost
# or for development purposes.
# When set to `true`, `SMTP_USERNAME` and `SMTP_PASSWORD` will be
# ignored and SMTP_PORT will default to 1025.
#
# default: false
# overwritten by: SMTP_DANGER_INSECURE
#danger_insecure = false

[encryption]
# You need to define at least one valid encryption key.
# These keys are used in various places, like for instance
# encrypting confidential client secrets in the database, or
# encryption cookies, and so on.
#
# The first part until the first `/` is the key ID.
# The ID must match '^[a-zA-Z0-9:_-]{2,20}$'
#
# The key itself begins after the first `/` has been found.
# The key must be exactly 32 bytes long, encoded as base64.
#
# You can find a more detailed explanation on how to generate
# keys in the documentation:
# 1. https://sebadob.github.io/rauthy/getting_started/k8s.html#create-and-apply-secrets
# 2. https://sebadob.github.io/rauthy/config/encryption.html
#
# You can provide multiple keys to make things like key
# rotation work. Be careful with removing old keys. Make sure
# that all secrets have been migrated beforehand.
# You can find a utility in the Admin UI to do this for you.
#
# overwritten by: ENC_KEYS - single String, \n separated values
keys = [
    'q6u26onRvXVG4427/M0NFQzhSSldCY01rckJNa1JYZ3g2NUFtSnNOVGdoU0E=',
    'bVCyTsGaggVy5yqQ/UzluN29DZW41M3hTSkx6Y3NtZmRuQkR2TnJxUTYzcjQ=',
]

# This identifies the key ID from the `ENC_KEYS` list, that
# should actively be used for new encryptions.
#
# overwritten by: ENC_KEY_ACTIVE
key_active = 'bVCyTsGaggVy5yqQ'

[ephemeral_clients]
# Can be set to 'true' to allow the dynamic client lookup via
# URLs as 'client_id's during authorization_code flow initiation.
#
# default: false
# overwritten by: ENABLE_EPHEMERAL_CLIENTS
#enable = false

# Can be set to 'true' to enable WebID functionality like needed
# for things like Solid OIDC.
#
# default: false
# overwritten by: ENABLE_WEB_ID
#enable_web_id = false

# If set to 'true', 'solid' will be added to the 'aud' claim from
# the ID token for ephemeral clients.
#
# default: false
# overwritten by: ENABLE_SOLID_AUD
#enable_solid_aud = false

# If set to 'true', MFA / Passkeys will be forced for ephemeral
# clients.
#
# default: false
# overwritten by: EPHEMERAL_CLIENTS_FORCE_MFA
#force_mfa = false

# The allowed flows for ephemeral clients.
#
# default: ['authorization_code', 'refresh_token']
# overwritten by: EPHEMERAL_CLIENTS_ALLOWED_FLOWS - single String, \n separated values
#allowed_flows = ['authorization_code', 'refresh_token']

# The allowed scopes separated by ' ' for ephemeral clients.
#
# default: ['openid', 'profile', 'email', 'webid']
# overwritten by: EPHEMERAL_CLIENTS_ALLOWED_SCOPES - single String, \n separated values
#allowed_scopes = ['openid', 'profile', 'email', 'webid']

# The lifetime in seconds ephemeral clients will be kept inside
# the cache.
#
# default: 3600
# overwritten by: EPHEMERAL_CLIENTS_CACHE_LIFETIME
#cache_lifetime = 3600

[events]
# The E-Mail address event notifications should be sent to.
#
# overwritten by: EVENT_EMAIL
email = 'admin@localhost'

# Matrix variables for event notifications.
# `matrix_user_id` and `matrix_room_id` are mandatory.
# Depending on your Matrix setup, additionally one of
# `matrix_access_token` or `matrix_user_password` is needed.
#
# If you log in to Matrix with User + Password, you may use
# `matrix_user_password`. If you log in via OIDC SSO (or just
# want to use a session token you can revoke), you should
# provide `matrix_access_token`.
# If both are given, the `matrix_access_token` will be preferred.
#
# If left empty, no messages will not be sent to Matrix.
# Format: `@&lt;user_id&gt;:&lt;server address&gt;`
#
# overwritten by: EVENT_MATRIX_USER_ID
#matrix_user_id = ''
# Format: `!&lt;random string&gt;:&lt;server address&gt;`
# overwritten by: EVENT_MATRIX_ROOM_ID
#matrix_room_id = ''
# overwritten by: EVENT_MATRIX_ACCESS_TOKEN
#matrix_access_token = ''
# overwritten by: EVENT_MATRIX_USER_PASSWORD
#matrix_user_password = ''
# URL of your Matrix server.
# default: https://matrix.org
# overwritten by: EVENT_MATRIX_SERVER_URL
#matrix_server_url = 'https://matrix.org'

# Optional path to a PEM Root CA certificate file for the
# Matrix client.
#
# overwritten by: EVENT_MATRIX_ROOT_CA_PATH
#matrix_root_ca_path = 'tls/root.cert.pem'

# May be set to disable the TLS validation for the Matrix
# client.
#
# default: false
# overwritten by: EVENT_MATRIX_DANGER_DISABLE_TLS_VALIDATION
#matrix_danger_disable_tls_validation = false

# The default behavior is, that Rauthy will panic at startup
# if it cannot connect to a configured Matrix server. The
# reason is that event notifications cannot be dropped silently.
#
# However, if you use a self-hosted Matrix server which uses
# Rauthy as its OIDC provider and both instances went offline,
# you will have a chicken-and-egg problem:
# - Rauthy cannot connect to Matrix and will panic
# - Your Matrix server cannot connect to Rauthy and will panic
# To solve this issue, you can temporarily set this value to
# 'true' and revert back, after the system is online again.
#
# default: false
# overwritten by: EVENT_MATRIX_ERROR_NO_PANIC
#matrix_error_no_panic = false

# The Webhook for Slack Notifications.
# If left empty, no messages will be sent to Slack.
#
# overwritten by: EVENT_SLACK_WEBHOOK
#slack_webhook = ''

# The notification level for events. Works the same way as
# a logging level. For instance: 'notice' means send out a
# notifications for all events with the notice level or higher.
# Possible values:
# - info
# - notice
# - warning
# - critical
#
# default: 'warning'
# overwritten by: EVENT_NOTIFY_LEVEL_EMAIL
notify_level_email = 'warning'
# default: 'notice'
# overwritten by: EVENT_NOTIFY_LEVEL_MATRIX
notify_level_matrix = 'notice'
# default: 'notice'
# overwritten by: EVENT_NOTIFY_LEVEL_SLACK
notify_level_slack = 'notice'

# Define the level from which on events should be persisted
# inside the database. All events with a lower level will be
# lost, if there is no active event subscriber.
# Possible values:
# - info
# - notice
# - warning
# - critical
#
# default: 'info'
# overwritten by: EVENT_PERSIST_LEVEL
#persist_level = 'info'

# Define the number of days when events should be cleaned
# up from the database.
#
# default: 30
# overwritten by: EVENT_CLEANUP_DAYS
#cleanup_days = 30

# Can be set to `false` to disable events being generated
# when a new token was issued. These events improve your
# auditing, but they can also be considered spam if you
# have a huge amount of users and logins.
#
# default: true
# overwritten_by: EVENT_GENERATE_TOKEN_ISSUED
#generate_token_issued = true

# The level for the generated Event after a backchannel
# logout has failed after exceeding all retries.
#
# default: critical
# overwritten by: EVENT_LEVEL_BACKCHANNEL_LOGOUT_FAILED
level_backchannel_logout_failed = 'critical'
# The level for the generated Event after a user was
# force-logged-out by an admin.
#
# default: notice
# overwritten by: EVENT_LEVEL_FORCE_LOGOUT
level_force_logout = 'notice'
# The level for the generated Event after an IP has
# been blacklisted
#
# default: warning
# overwritten by: EVENT_LEVEL_IP_BLACKLISTED
level_ip_blacklisted = 'warning'
# The level for the generated Event after the JWKS has
# been rotated
#
# default: notice
# overwritten by: EVENT_LEVEL_JWKS_ROTATE
level_jwks_rotate = 'notice'
# The level for the generated Event after a login from
# a new location for a user.
#
# default: notice
# overwritten by: EVENT_LEVEL_NEW_LOGIN_LOCATION
level_new_login_location = 'notice'
# The level for the generated Event after a new user has
# been registered.
#
# default: info
# overwritten by: EVENT_LEVEL_NEW_USER
level_new_user = 'info'
# The level for the generated Event after a user has
# changed his E-Mail
#
# default: notice
# overwritten by: EVENT_LEVEL_USER_EMAIL_CHANGE
level_user_email_change = 'notice'
# The level for the generated Event after a user has
# reset its password
#
# default: notice
# overwritten by: EVENT_LEVEL_USER_PASSWORD_RESET
level_user_password_reset = 'notice'
# The level for the generated Event after a user has
# been given the 'rauthy_admin' role
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_ADMIN
level_rauthy_admin = 'notice'
# The level for the generated Event after a Rauthy
# entered a healthy state (again)
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_HEALTHY
level_rauthy_healthy = 'notice'
# The level for the generated Event after a Rauthy
# instance has been started
#
# default: info
# overwritten by: EVENT_LEVEL_RAUTHY_START
level_rauthy_start = 'info'
# The level for the generated Event after a Rauthy
# entered an unhealthy state
#
# default: critical
# overwritten by: EVENT_LEVEL_RAUTHY_UNHEALTHY
level_rauthy_unhealthy = 'critical'
# The level for the generated Event after a new App
# version has been found
#
# default: notice
# overwritten by: EVENT_LEVEL_RAUTHY_VERSION
level_rauthy_version = 'notice'
# The level for the generated Event after DB secrets
# have been migrated to a new key
#
# default: notice
# overwritten by: EVENT_LEVEL_SECRETS_MIGRATED
level_secrets_migrated = 'notice'
# The level for the generated Event after a user revoked
# an illegal login with the link from the notification
# E-Mail for "login from unknown location"
#
# default: warning
# overwritten by: EVENT_LEVEL_USER_LOGIN_REVOKE
level_user_login_revoke = 'warning'
# The level for the generated Event after a SCIM
# update task ultimately failed after exceeding
# all retries.
#
# default: critical
# overwritten by: EVENT_LEVEL_SCIM_TASK_FAILED
level_scim_task_failed = 'critical'
# The level for the generated Event after Rauthy
# detected a suspicious API scanning request.
#
# default: notice
# overwritten by: EVENT_LEVEL_SUSPICIOUS_REQUEST
level_suspicious_request = 'notice'
# The level for the generated Event after a new JWT
# Token was issued.
#
# default: info
# overwritten by: EVENT_LEVEL_TOKEN_ISSUED
level_token_issued = 'info'

# The level for the generated Event after certain
# amounts of false logins from an IP
#
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_25
level_failed_logins_25 = 'critical'
# default: critical
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_20
level_failed_logins_20 = 'critical'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_15
level_failed_logins_15 = 'warning'
# default: warning
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_10
level_failed_logins_10 = 'warning'
# default: notice
# overwritten by: EVENT_LEVEL_FAILED_LOGINS_7
level_failed_logins_7 = 'notice'
# default: info
# overwritten by: EVENT_LEVEL_FAILED_LOGIN
level_failed_login = 'info'

# If set to 'true', it will disable the app version
# checker. This is a scheduled task that looks up the
# latest version periodically by doing a request to the
# Github API to check the latest release. This ignores
# any type of prerelease and will only notify for a new stable.
#
# default: false
# overwritten by: DISABLE_APP_VERSION_CHECK
#disable_app_version_check = false

[fedcm]
## CAUTION: The FedCM is highly experimental at this point!
## Do not attempt to use it in production because it is
## subject to change in the future, or it may be dropped
## completely, depending on how Googles goes on with it!
## The spec is currently a draft and under active development.

# Set to `true` to enable the experimental FedCM.
# default: false
# overwritten by: EXPERIMENTAL_FED_CM_ENABLE
#experimental_enable = false

# Session lifetime for FedCM in seconds - the session can not be
# extended beyond this time and a new login will be forced.
#
# default: 2592000
# overwritten by: SESSION_LIFETIME_FED_CM
#session_lifetime = 2592000

# Session timeout for FedCM in seconds
# When a new token / login is requested before this timeout hits
# the limit, the user will be authenticated without prompting
# for the credentials again. This is the value which can extend
# the session, until it hits its maximum lifetime set with _FED_CM.
#
# default: 259200
# overwritten by: SESSION_TIMEOUT_FED_CM
#session_timeout = 259200

[geolocation]
# If you have a configured and working Geolocation setup, you can
# define if un-resolvable IP addresses should be blocked. By default,
# if a Country cannot be found for a certain IP address, it will be
# allowed anyway. This is necessary to allow private network connections
# for instance. Only if you run a public Rauthy instance, and you will
# guaranteed always connect with a public Peer IP, you might want to
# set this to `true`.
#
# default: false
# overwritten by: GEO_BLOCK_UNKONW
#block_unknown = false

# If you have a WAF or CDN which injects a geoloaction header
# with the country code, provide the name here. For instance,
# in case of Cloudflare, this would be 'CF-IPCountry'.
#
# This header will only be accepted, if Rauthy runs in proxy_mode,
# and the source IP is a trusted proxy, to prevent spoofing.
#
# default: not set
# overwritten by: GEO_COUNTRY_HEADER
#country_header = 'CF-IPCountry'

# You can black- or whitelist countries, if you have a configured
# and working Geolocation, either via `country_header` or a
# Maxmind database.
#
# The `country_list_type` can be either `whitelist` or `blacklist`,
# and it will specify the behavior of the `country_list`.
# For instance, if you have `country_list_type = 'whitelist'` and
# `country_list = ['DE', 'FR']`, only access from Germany and France
# will be allowed.
#
# The `whitelist` type is a `default-deny`, while `blacklist` is
# `default-allow`.
#
# If `country_list_type` is not set at all, Geoblocking will be
# disabled.
#
# default: not set
#country_list_type = 'whitelist'
# default: not set
#country_list = []

# If you don't have a header with a country code, you can
# also provide a Maxmind account. Rauthy will then download
# the 'GeoLite2 Country' database regularly and use it for
# geolocating IPs.
#
# The GeoLite databases from Maxmind are free and published
# under the Creative Commons License. You can also provide
# an Enterprise database, which will have more accurate data.
# Check the `maxmind_db_type` below.
#
# default: not set
# overwritten by: GEO_MAXMIND_ACC_ID
#maxmind_account_id = ''
# overwritten by: GEO_MAXMIND_LICENSE
#maxmind_license_key = ''

# If `maxmind_account_id` and `maxmind_license_key`, this
# will be the directory being used for DB download and storage.
#
# default: 'data'
# overwritten by: GEO_MAXMIND_DIR
#maxmind_db_dir = 'data'

# By default, the `GeoLite2-Country` database from Maxmind is
# being used. The IP Geolocation databases are loaded fully into
# memory at startup to speedup lookups. The size therefore makes
# a big difference, not only for lookup speed, but also in terms
# of memory usage. The Country DB adds ~10MB of memory overhead,
# while the City DB is around 65MB.
#
# Possible Values (case-sensitive):
# - GeoLite2-Country
# - GeoLite2-City
#
# If you have access to paid Maxmind databases, you can add the
# db_type in a way that it resolves to a valid download link.
# The link will be created with the following template:
# `https://download.maxmind.com/geoip/databases/{maxmind_db_type}/download?suffix=tar.gz`
#
# default: 'GeoLite2-Country'
# overwritten by: GEO_MAXMIND_DB_TYPE
#maxmind_db_type = 'GeoLite2-Country'

# If you configured a `maxmind_account_id` + `maxmind_license_key`,
# you can change the time when the DB update job runs. By default,
# it runs every night at 05:00. It will check if y new version of
# the MaxMind DB is available and if so, download it.
#
# Accepts cron syntax:
# "sec min hour day_of_month month day_of_week year"
#
# default: "0 0 5 * * * *"
# overwritten by: GEO_MAXMIND_UPDATE_CRON
#maxmind_update_cron = "0 0 5 * * * *"

[hashing]
# Argon2ID hashing parameters. Take a look at the documentation
# for more information:
# https://sebadob.github.io/rauthy/config/argon2.html
# M_COST must never be below 32768 in production
# overwritten by: ARGON2_M_COST
argon2_m_cost = 131072
# T_COST must never be below 1 in production
# overwritten by: ARGON2_T_COST
argon2_t_cost = 4
# P_COST must never be below 2 in production
# overwritten by: ARGON2_P_COST
argon2_p_cost = 8

# Limits the maximum amount of parallel password hashes at the exact same time
# to never exceed system memory while still allowing a good amount of memory
# for the Argon2ID algorithm
#
# CAUTION: You must make sure, that you have at least
# (MAX_HASH_THREADS * ARGON2_M_COST / 1024) + idle memory of your deployment available.
#
# default: 2
# overwritten by: MAX_HASH_THREADS
max_hash_threads = 2

# The time in ms when to log a warning, if a request waited longer than this time.
# This is an indicator, that you have more concurrent logins than allowed and may
# need config adjustments,
# if this happens more often.
#
# default: 500
# overwritten by: HASH_AWAIT_WARN_TIME
#hash_await_warn_time = 500

[http_client]
## In this section, you can configure the HTTP Client
## that Rauthy uses for all different kind's of tasks,
## like e.g. fetching ephemeral client information,
## remote JWKS, connecting to Upstream Auth Providers,
## or for Slack notifications.
##
## NOTE: The only exception is the Matrix Event
## Notification client, if you have this configured.
## Because of the internal structure of Matrix dependencies,
## this cannot easily re-use the global client!
##
## Rauthy creates a single Lazily Initialized instance
## with connection pooling to reduce the amount of TLS
## handshakes necessary, especially during high traffic.

# The connect timeout in seconds for new connections.
#
# default: 10
# overwritten by: HTTP_CONNECT_TIMEOUT
#connect_timeout = 10

# The total request timeout in seconds for all outgoing
# requests.
#
# default: 10
# overwritten by: HTTP_REQUEST_TIMEOUT
#request_timeout = 10

# Set the min TLS version for all outgoing connections.
# Allowed values: '1.3', '1.2', '1.1', '1.0'
#
# default: '1.3'
# overwritten by: HTTP_MIN_TLS
#min_tls = '1.3'

# The duration in seconds for idle connections in the pool.
# To reduce memory consumption slightly, you may reduce
# this value at the cost of needing more TLS handshakes
# and re-connecting more often.
#
# default: 900
# overwritten by: HTTP_IDLE_TIMEOUT
#idle_timeout = 900

# By default, the HTTP Client will enforce HTTPS and
# simply fail if an unencrypted HTTP URL is given
# anywhere.
# However, you can allow this by setting `true`.
#
# default: false
# overwritten by: HTTP_DANGER_UNENCRYPTED
#danger_unencrypted = false

# By default, the HTTP Client will enforce valid TLS
# certificates and simply fail if an invalid certificate
# is used anywhere.
# However, you can allow this by setting `true`.
#
# default: false
# overwritten by: HTTP_DANGER_INSECURE
#danger_insecure = false

# You can provide a root certificate bundle, if you
# are running servers / clients Rauthy needs to connect
# to with self-signed certificates.
# The certificates need to be in PEM format.
#
# overwritten by: HTTP_CUST_ROOT_CA_BUNDLE
#root_ca_bundle = """
#-----BEGIN CERTIFICATE-----
#...
#-----END CERTIFICATE-----
#-----BEGIN CERTIFICATE-----
#....
#-----END CERTIFICATE-----
#"""

[i18n]
# Can be set to filter the languages to show in the UI. If not
# set, all available i18n translations will be shown in the
# language selector.
# To show only specific ones, provide them here as a
# space-separated value.
#
# Languages for all user-facing pages.
# Available Options: en de ko nb zhhans
# overwritten by: FILTER_LANG_COMMON
filter_lang_common = ['en', 'de', 'ko', 'nb', 'zhhans']
# Languages for the Admin UI.
# Available Options: en de ko nb
# overwritten by: FILTER_LANG_ADMIN
filter_lang_admin = ['en', 'de', 'ko', 'nb']

[lifetimes]
# Set the grace time in seconds for how long in seconds the refresh
# token should still be valid after usage. Keep this value small,
# but do not set it to 0 with an HA deployment to not get issues with
# small HA cache latencies.
#
# If you have an external client, which does concurrent requests, from
# which the request interceptor wants to refresh the token, you may
# have multiple hits on the endpoint and all of them should be valid.
#
# Caching is done on the endpoint itself, but grace time of 0 will only
# be good for a single instance of rauthy.
#
# default: 5
# overwritten by: REFRESH_TOKEN_GRACE_TIME
#refresh_token_grace_time = 5

# Global default lifetime in hours for refresh tokens.
#
# default: 48
# overwritten by: REFRESH_TOKEN_LIFETIME
#refresh_token_lifetime = 48

# Session lifetime in seconds - the session can not be extended
# beyond this time and a new login will be forced. This is the
# session for the authorization code flow.
#
# default: 14400
# overwritten by: SESSION_LIFETIME
#session_lifetime = 14400

# If 'true', a 2FA / MFA check will be done with each automatic
# token generation, even with an active session, which kind of
# makes the session useless with Webauthn enabled, but provides
# maximum amount of security. If 'false', the user will not get
# an MFA prompt with an active session at the authorization endpoint.
#
# default: false
# overwritten by: SESSION_RENEW_MFA
#session_renew_mfa = false

# Session timeout in seconds. When a new token / login is requested
# before this timeout hits the limit, the user will be authenticated
# without prompting for the credentials again.
#
# This is the value which can extend the session, until it hits its
# maximum lifetime set with session_lifetime.
#
# default: 5400
# overwritten by: SESSION_TIMEOUT
#session_timeout = 5400

# Lifetime in minutes for password reset magic links.
#
# default: 30
# overwritten by: ML_LT_PWD_RESET
#magic_link_pwd_reset = 30

# Lifetime in minutes for the first password magic link,
# for setting the initial password.
#
# default: 4320
# overwritten by: ML_LT_PWD_FIRST
#magic_link_pwd_first = 4320

# JWKS auto rotate cronjob. This will (by default) rotate all JWKs every
# 1. day of the month. If you need smaller intervals, you may adjust this
# value. For security reasons, you cannot fully disable it.
# In a HA deployment, this job will only be executed on the current cache
# leader at that time.
# Format: "sec min hour day_of_month month day_of_week year"
#
# default: "0 30 3 1 * * *"
# overwritten by: JWK_AUTOROTATE_CRON
#jwk_autorotate_cron = '0 30 3 1 * * *'

[logging]
# This is the log level for stdout logs
# Accepts: error, warn, info, debug, trace
#
# default: 'info'
# overwritten by: LOG_LEVEL
#level = 'info'

# The log level for the `Hiqlite` persistence layer.
# At the time of writing, only the cache will use `hiqlite`
#
# default: info
# overwritten by: LOG_LEVEL_DATABASE
#level_database = 'info'

# This is a special config which allows the configuration of
# customized access logs. These logs will be logged with each
# request in addition to the normal LOG_LEVEL logs.
# The following values are valid:
# - `debug`
#   CAUTION: The Debug setting logs every information available
#   to the middleware which includes SENSITIVE HEADERS
#   DO NOT use the Debug level in a working production environment!
# - `verbose`
#   Verbose logging without headers - generates huge outputs
# - `basic`
#   Logs access to all endpoints apart from the Frontend ones which
#   all js, css, ...
# - `modifying`
#   Logs only requests to modifying endpoints and skips all GET
# - `off`
#
# default: 'modifying'
# overwritten by: LOG_LEVEL_ACCESS
level_access = 'modifying'

# You can change the log output format to JSON, if you set:
# `log_fmt=json`.
# Keep in mind, that some logs will include escaped values,
# for instance when `Text` already logs a JSON in debug level.
# Some other logs like an Event for instance will be formatted
# as Text anyway. If you need to auto-parse events, please consider
# using an API token and listen ot them actively.
#
# default: text
# overwritten by: LOG_FMT
#log_fmt = 'json'

[mfa]
# If 'true', MFA for an account must be enabled to access the
# rauthy admin UI.
#
# default: true
# overwritten by: ADMIN_FORCE_MFA
admin_force_mfa = true

[pam]
# The length of newly generated PAM remote passwords via the
# account dashboard. The default is fine as long as you can copy
# &amp; paste them. You may want to reduce the length here if you e.g.
# occasionally generate them on mobile and need to type them
# manually into some terminal.
#
# default: 24
# overwritten by: PAM_REMOTE_PASSWORD_LEN
#remote_password_len = 24

# The TTL for newly generated PAM remote passwords in seconds.
# The default gives you plenty of time to open a few sessions in
# some terminals and maybe switch to `root` on some remote machines,
# while still expiring quick enough to be secure.
#
# default: 120
# overwritten by: PAM_REMOTE_PASSWORD_TTL
#remote_password_ttl = 120

[pow]
# The difficulty for a Proof-of-Work (PoW).
# The default is 20, which is reasonable for modern processors.
# This is currently only used for the user registration via UI.
# The value must be between 10 and 99.
#
# default: 19
# overwritten by: POW_DIFFICULTY
#difficulty = 19

# The expiration duration in seconds for a PoW
#
# default: 30
# overwritten by: POW_EXP
#exp = 30

[scim]
# If set to `true`, already possibly synced groups / users on a
# SCIM server may be deleted if either sync if disabled further
# down the road.
# Depending on your clients specifically, deleting a group / user
# can have a major impact, like e.g. maybe the user had important
# data inside that application that would be deleted automatically
# as well. Depending on your use case, you may want to handle
# deletions manually.
#
# If set to false, only the `externalId` field would be removed
# for the targeted resources, but they would not be fully deleted.
# This will basically unlink the resource from Rauthy, leaving it
# behind independent.
#
# default: false
# overwritten by: SCIM_SYNC_DELETE_GROUPS
sync_delete_groups = false
# overwritten by: SCIM_SYNC_DELETE_USERS
sync_delete_users = false

# The maximum amount of retries made for a failed SCIM task.
# Failed tasks will be retried every 60 - 90 seconds from all
# cluster nodes. The timeout between retries is randomized to
# avoid overloading clients. It will be executed on each cluster
# member to increase the chance of a update in case of network
# segmentations.
#
# default: 100
# overwritten by: SCIM_RETRY_COUNT
#retry_count = 100

[server]
# The server address to listen on. Can bind to a specific IP.
# When LISTEN_SCHEME is set to use UNIX sockets, this specifies
# the path to listen on.
#
# default: '0.0.0.0'
# overwritten by: LISTEN_ADDRESS
listen_address = '0.0.0.0'

# The listen ports for HTTP / HTTPS, depending on the activated
# 'LISTEN_SCHEME'
#
# default: 8080
# overwritten by: LISTEN_PORT_HTTP
port_http = 8080
# default: 8443
# overwritten by: LISTEN_PORT_HTTPS
port_https = 8443

# The scheme to use locally, valid values:
# http | https | http_https | unix_http | unix_https
# For more details about the UNIX domain socket, check out its
# documentation page.
#
# default: http_https
# overwritten by: LISTEN_SCHEME
scheme = 'http'

# The Public URL of the whole deployment
# The LISTEN_SCHEME + PUB_URL must match the HTTP ORIGIN HEADER
# later on, which is especially important when running Rauthy
# behind a reverse proxy. In case of a non-standard port (80/443),
# you need to add the port to the PUB_URL
#
# default: not set - mandatory
# overwritten by: PUB_URL
pub_url = 'localhost:8080'

# Limits the amount of HTTP worker threads. This value
# heavily impacts memory usage, even in idle. The default
# values are:
# - less than 4 CPU cores -&gt; 1
# - 4+ cores -&gt; max(2, cores - MAX_HASH_THREADS - reserve)
#   where `reserve` is 2 when `HIQLITE=true` and 1 otherwise.
#
# CAUTION: If you run your instance on a big underlying host,
# you almost always want to manually set an appropriate
# value. Rauthy can only see all available cores and not any
# possibly set container limits. This means if it runs inside
# a container on something like a 96 core host, Rauthy will
# by default spawn very many threads.
#
# overwritten by: HTTP_WORKERS
#http_workers = 1

# When rauthy is running behind a reverse proxy, set to true
#
# default: false
# overwritten by: PROXY_MODE
proxy_mode = false

# A list of trusted proxy CIDRs. When `proxy_mode = true`
# or `peer_ip_header_name` is set, these are mandatory to
# be able to extract the real client IP properly and safely
# to prevent IP header spoofing. All requests witha
# different source will be blocked.
#
# default: []
# overwritten by: TRUSTED_PROXIES - single String, \n separated values
#trusted_proxies = ['192.168.0.0/24']

# To bring support for applications using deep-linking, you
# can set custom URL schemes to be accepted when present in
# the `Origin` header. For instance, a Tauri app would set
# `tauri://` instead of `https://`.
#
# Provide the value as a space separated list of Strings,
# like for instance: ["tauri", "myapp"]
#
# default: []
# overwritten by: ADDITIONAL_ALLOWED_ORIGIN_SCHEMES - single String, \n separated values
#additional_allowed_origin_schemes = ['tauri', 'myapp']

# To enable or disable the additional HTTP server to expose
# the /metrics endpoint.
#
# default: false
# overwritten by: METRICS_ENABLE
metrics_enable = false

# The IP address to listen on for the /metrics endpoint.
# You do not want to expose your metrics on a publicly
# reachable endpoint!
#
# default: '0.0.0.0'
# overwritten by: METRICS_ADDR
#metrics_addr = '0.0.0.0'

# The port to listen on for the /metrics endpoint.
# You do not want to expose your metrics on a publicly
# reachable endpoint!
#
# default: 9090
# overwritten by: METRICS_PORT
#metrics_port = 9090

# Can be set to `true` to enable the Swagger UI.
# This will consume ~13mb of additional memory.
#
# default: false
# overwritten by: SWAGGER_UI_ENABLE
swagger_ui_enable = false

# Can be set to `true` to make the Swagger UI publicly
# available. By default, you can only access it with a
# valid `rauthy_admin` session.
#
# default: false
# overwritten by: SWAGGER_UI_PUBLIC
#swagger_ui_public = false

# The interval in seconds in which keep-alives should be
# sent to SSE clients. Depending on your network setup,
# proxy timeouts, ..., you may adjust this value to fit
# your needs.
#
# default: 30
# overwritten by: SSE_KEEP_ALIVE
#see_keep_alive = 30

# Dynamic server side pagination threshold
# If the total users count exceeds this value, Rauthy will dynamically
# change search and pagination for users in the Admin UI from client
# side to server side to not have a degradation in performance.
#
# default: 1000
# overwritten by: SSP_THRESHOLD
#ssp_threshold = 1000

[suspicious_requests]
# The "catch all" route handler on `/` will compare the request
# path against a hardcoded list of common scan targets from bots
# and attackers. If the path matches any of these targets, the
# IP will be blacklisted preemptively for the set time in minutes.
# You can disable it by setting it to `0`.
#
# default: 1440
# overwritten by: SUSPICIOUS_REQUESTS_BLACKLIST
#blacklist = 1440

# Will emit a log with level of warning if a request to `/` has
# been made that has not been caught by any of the usual routes
# and handlers. Apart from a request to just `/` which will end
# in a redirect to `/auth/v1`, all additional path's will be
# logged. This can help to improve the internal suspicious
# blocklist in the future.
#
# default: false
# overwritten by: SUSPICIOUS_REQUESTS_LOG
#log = false

[[templates]]
# You can overwrite some default email templating values.
# If you want to modify the basic templates themselves, this is
# only possible with a custom build from source. The content
# however can mostly be set here.
# If the below values are not set, the default will be taken.
#
# NOTE: This is an array value, and you can specify it multiple
# times for different `lang` / `typ` combinations.

# one of: en de ko zh_hans
#lang = 'en'

# pme of: password_new, password_reset
#typ = 'password_new'

#subject = 'New Password'
#header = 'New password for'
#text = ''
#click_link = 'Click the link below to get forwarded to the password form.'
#validity = 'This link is only valid for a short period of time for security reasons.'
#expires = 'Link expires:'
#footer = ''

[tls]
## UI + API TLS

# Overwrite the path to the TLS certificate file in PEM
# format for rauthy
#
#default: 'tls/tls.crt'
# overwritten by: TLS_CERT
cert_path = 'tls/cert-chain.pem'

# Overwrite the path to the TLS private key file in PEM
# format for rauthy. If the path / filename ends with '.der',
# rauthy will parse it as DER, otherwise as PEM.
#
# default: 'tls/tls.key'
# overwritten by: TLS_KEY
key_path = 'tls/key.pem'

# If set to `true`, Rauthy will generate self-signed TLS certs and copy
# them into `tls/self_signed_cert.pem` and `tls/self_signed_key.pem`.
# It will also IGNORE any `cert_path` / `key_path`.
#
# CAUTION: If set to `true`, it will delete existing files:
# - `tls/self_signed_cert.pem`
# - `tls/self_signed_key.pem`
#
# This should only be used for testing and never in production!
#
# default: false
# overwritten by: TLS_GENERATE_SELF_SIGNED
#generate_self_signed = false

[user_pictures]
# The storage type for user pictures.
# By default, they are saved inside the Database, which is not ideal.
# If you only have a couple hundred users, this will be fine, but
# anything larger should use an S3 bucket if available. For single
# instance deployments, file storage to local disk is available
# as well, but this must not be used with multi replica / HA
# deployments.
# Images will ba reduced in size to max 192px on the longest side.
# They most often end up between 25 - 40kB in size.
#
# Available options: db file s3 disabled
# Default: db
# overwritten by: PICTURE_STORAGE_TYPE
storage_type = 'db'

# If `storage_type = file`, the path where pictures will be saved
# can be changed with this value.
#
# default: ./pictures
# overwritten by: PICTURE_PATH
#path = './pictures'

# Access values for the S3 bucket if `PICTURE_STORAGE_TYPE=s3`.
# Not needed otherwise.
# overwritten by: PIC_S3_URL
#s3_url = 'https://s3.example.com'
# overwritten by: PIC_S3_BUCKET
#bucket = 'my_bucket'
# overwritten by: PIC_S3_REGION
#region = 'example'
# overwritten by: PIC_S3_KEY
#s3_key = 's3_key'
# overwritten by: PIC_S3_SECRET
#s3_secret = 's3_secret'
# default: true
# overwritten by: PIC_S3_PATH_STYLE
#s3_path_style = true

# Set the upload limit for user picture uploads in MB.
#
# default: 10
# overwritten by: PICTURE_UPLOAD_LIMIT_MB
#upload_limit_mb = 10

# By default, user pictures can only be fetched with a valid
# session, an API Key with access to Users + Read, or with a
# valid token for this user. However, depending on where and
# how you are using Rauthy for your user management, you may
# want to make user pictures available publicly without any
# authentication.
#
# User Picture URLs are "safe" in a way that you cannot guess
# a valid URL. You will need to know the User ID + the Picture
# ID. Both values are generated cryptographically secure in the
# backend during creation. The Picture ID will also change
# with every new upload.
#
# default: false
# overwritten by: PICTURE_PUBLIC
#public = false

[user_registration]
# If the User Registration endpoint should be accessible by anyone.
# If not, an admin must create each new user.
#
# default: false
# overwritten by: OPEN_USER_REG
#enable = false

# Can be used when 'open_user_reg = true' to restrict the domains for
# a registration. For instance, set it to
# 'user_reg_domain_restriction = gmail.com' to allow only
# registrations with 'user@gmail.com'.
#
# overwritten by: USER_REG_DOMAIN_RESTRICTION
#domain_restriction = 'my-domain.com'

# If `open_user_reg = true`, you can blacklist certain domains on
# the open registration endpoint.
#
# default: []
# overwritten by: USER_REG_DOMAIN_BLACKLIST - single String, \n separated values
#domain_blacklist = [
#    'example.com',
#    'evil.net',
#]

# If set to `true`, any validation of the `redirect_uri` provided
# during a user registration will be disabled. Clients can use
# this feature to redirect the user back to their application
# after a successful registration, so instead of ending up in the
# user dashboard, they come back to the client app that initiated
# the registration.
#
# The given `redirect_uri` will be compared against all registered
# `client_uri`s and will throw an error, if there is no match.
# However, this check will prevent ephemeral clients from using
# this feature. Only if you need it in combination with ephemeral
# clients, you should set this option to `true`. Otherwise, it is
# advised to set the correct Client URI in the admin UI. The
# `redirect_uri` will be allowed if it starts with any registered
# `client_uri`.
#
# default: false
# overwritten by: USER_REG_OPEN_REDIRECT
#allow_open_redirect = false

[webauthn]
# The 'Relaying Party (RP) ID' - effective domain name.
#
# CAUTION: When this changes, already registered devices will
# stop working and users cannot log in anymore!
#
# default: 'localhost'
# overwritten by: RP_ID
rp_id = 'localhost'

# Url containing the effective domain name.
#
# DEV: If you want to test Webauthn via the Svelte DEV UI,
# change the port number to :5173.
#
# !!! CAUTION: Must ALWAYS include the port number !!!
#
# default: 'http://localhost:8080'
# overwritten by: RP_ORIGIN
rp_origin = 'http://localhost:8080'

# Non-critical RP Name
# Has no security properties and may be changed without issues
#
# default: 'Rauthy IAM'
# overwritten by: RP_NAME
#rp_name = 'Rauthy IAM'

# The Cache lifetime in seconds for Webauthn requests. Within
# this time, a webauthn request must have been validated.
#
# default: 60
# overwritten by: WEBAUTHN_REQ_EXP
#req_exp = 60

# The Cache lifetime for additional Webauthn Data like auth
# codes and so on. Should not be lower than WEBAUTHN_REQ_EXP.
# The value is in seconds
#
# default: 90
# overwritten by: WEBAUTHN_DATA_EXP
#data_exp = 90

# With Webauthn enabled for a user, he needs to enter username
# / password on a new system. If these credentials are verified,
# Rauthy will set an additional cookie, which will determine
# how long the user can then use only (safe) MFA passwordless
# Webauthn login with Yubikeys, apple touch id, windows hello,
# ... until he needs to verify his credentials again.
#
# Passwordless login is generally much safer than logging in
# with a password. But sometimes it is possible, that the
# Webauthn devices do not force the user to include a second
# factor, which in that case would be a single factor login
# again. That is why we should ask for the original password in
# addition once in a while to set the cookie.
#
# The value is in hours
# default: 2160
# overwritten by: WEBAUTHN_RENEW_EXP
#renew_exp = 2160

# This feature can be set to 'true' to force User verification
# during the Webauthn ceremony. UV will be true, if the user
# does not only need to verify its presence by touching the key,
# but by also providing proof that he knows (or is) some secret
# via a PIN or biometric key for instance. With UV, we have a
# true MFA scenario where UV == false (user presence only) would
# be a 2FA scenario (with password).
#
# Be careful with this option, since Android and some special
# combinations of OS + browser to not support UV yet or may have
# weird edge cases, especially Android still has issues.
#
# default: false
# overwritten by: WEBAUTHN_FORCE_UV
#force_uv = true

# Can be set to 'true' to disable password expiry for users that
# have at least one active passkey. When set to 'false', the same
# password expiry from the set policy will apply to these users
# as well. With this option active, Rauthy will ignore any password
# expiry set by the password policy for Webauthn users.
#
# default: true
# overwritten by: WEBAUTHN_NO_PASSWORD_EXPIRY
#no_password_exp = true
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="swagger-ui"><a class="header" href="#swagger-ui">Swagger UI</a></h1>
<p>Rauthy comes with its own OpenAPI documentation and Swagger UI. There are some things you should know about it.</p>
<p>It is publicly exposed when you start Rauthy for testing locally, but by default it is not. Rauthy is open source and
anyone can simply start it and take a look at the API docs, but exposing the API docs in production to anyone is not a
good practice, especially because it might increase the attack surface.</p>
<p>By default, the SwaggerUI is disabled, because it requires ~13MB of memory at runtime. You can enable it via
configuration:</p>
<pre><code class="language-toml">[server]
# Can be set to `true` to enable the Swagger UI.
# This will consume ~13mb of additional memory.
#
# default: false
# overwritten by: SWAGGER_UI_ENABLE
swagger_ui_enable = true
</code></pre>
<div id="admonition-info" class="admonition admonish-info" role="note" aria-labelledby="admonition-info-title">
<div class="admonition-title">
<div id="admonition-info-title">
<p>Info</p>
</div>
<a class="admonition-anchor-link" href="swagger.html#admonition-info"></a>
</div>
<div>
<p>The UI is not meant to be used for testing out endpoints. Rauthy has way to many options and details regarding to the
authentication and it's not that simple, that the default Swagger UI can handle it, at least to my knowledge.</p>
<p>If you want to test certain endpoints, I suggest you <b>create an API Key</b> for that case, which you can use with any tool
you like.</p>
</div>
</div>
<div id="admonition-hint" class="admonition admonish-tip" role="note" aria-labelledby="admonition-hint-title">
<div class="admonition-title">
<div id="admonition-hint-title">
<p>Hint</p>
</div>
<a class="admonition-anchor-link" href="swagger.html#admonition-hint"></a>
</div>
<div>
<p>If you just want to take a look at the SwaggerUI when you have no running Rauthy instance yet, I have a test / dev
instance running which exposes the API docs:
<a href="https://iam.sebadob.dev/auth/v1/docs/" target="_blank">Swagger UI</a></p>
<p>Please note that this is a testing instance and it might not be online all the time or be running the latest nightly
build, but everything should be fine most of the time.</p>
</div>
</div>
<h2 id="external-access"><a class="header" href="#external-access">External Access</a></h2>
<p>If you open the Rauthy Admin UI and navigate to <code>Docs</code>, you will find a link to the Swagger UI that each Rauthy instance
hosts. By default, in production this will not be exposed and the link in the UI will not be working as mentioned in the
description in the same place.</p>
<p>If you don't care about exposing it publicly, or you need non-<code>rauthy_admin</code>s to have access, you can do so with the
following and make the link work:</p>
<pre><code class="language-toml">[server]
# Can be set to `true` to make the Swagger UI publicly
# available. By default, you can only access it with a
# valid `rauthy_admin` session.
#
# default: false
# overwritten by: SWAGGER_UI_PUBLIC
swagger_ui_public = false
</code></pre>
<h2 id="internal-access"><a class="header" href="#internal-access">Internal Access</a></h2>
<p>By defautl, the SwaggerUI is only visible to <code>rauthy_admin</code>s with a valid session. You can access it via the Admin UI,
or via <code>/auth/v1/docs/</code> on your Rauthy instance.</p>

                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->


                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">

            </nav>

        </div>




        <script>
            window.playground_copyable = true;
        </script>


        <script src="elasticlunr.min.js"></script>
        <script src="mark.min.js"></script>
        <script src="searcher.js"></script>

        <script src="clipboard.min.js"></script>
        <script src="highlight.js"></script>
        <script src="book.js"></script>

        <!-- Custom JS scripts -->

        <script>
        window.addEventListener('load', function() {
            window.setTimeout(window.print, 100);
        });
        </script>

    </div>
    </body>
</html>
